diff --git a/src/Cake.VisualStudio.csproj b/src/Cake.VisualStudio.csproj index 4e15588..5f8c2db 100644 --- a/src/Cake.VisualStudio.csproj +++ b/src/Cake.VisualStudio.csproj @@ -67,6 +67,10 @@ + + + + diff --git a/src/Editor/IndentationResult.cs b/src/Editor/IndentationResult.cs new file mode 100644 index 0000000..6b12e53 --- /dev/null +++ b/src/Editor/IndentationResult.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cake.VisualStudio.Editor +{ + /// + /// An indentation result represents where the indent should be placed. It conveys this through + /// a pair of values. A position in the existing document where the indent should be relative, + /// and the number of columns after that the indent should be placed at. + /// + /// This pairing provides flexibility to the implementor to compute the indentation results in + /// a variety of ways. For example, one implementation may wish to express indentation of a + /// newline as being four columns past the start of the first token on a previous line. Another + /// may wish to simply express the indentation as an absolute amount from the start of the + /// current line. With this tuple, both forms can be expressed, and the implementor does not + /// have to convert from one to the other. + /// + internal struct IndentationResult + { + /// + /// The base position in the document that the indent should be relative to. This position + /// can occur on any line (including the current line, or a previous line). + /// + public int BasePosition { get; } + + /// + /// The number of columns the indent should be at relative to the BasePosition's column. + /// + public int Offset { get; } + + public IndentationResult(int basePosition, int offset) : this() + { + this.BasePosition = basePosition; + this.Offset = offset; + } + } +} diff --git a/src/Editor/LineExtensions.cs b/src/Editor/LineExtensions.cs new file mode 100644 index 0000000..90e9685 --- /dev/null +++ b/src/Editor/LineExtensions.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.TextFormatting; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + +namespace Cake.VisualStudio.Editor +{ + internal static class ITextSnapshotLineExtensions + { + /// + /// Returns the first non-whitespace position on the given line, or null if + /// the line is empty or contains only whitespace. + /// + public static int? GetFirstNonWhitespacePosition(this ITextSnapshotLine line) + { + var text = line.GetText(); + + for (int i = 0; i < text.Length; i++) + { + if (!char.IsWhiteSpace(text[i])) + { + return line.Start + i; + } + } + + return null; + } + + /// + /// Returns the first non-whitespace position on the given line as an offset + /// from the start of the line, or null if the line is empty or contains only + /// whitespace. + /// + public static int? GetFirstNonWhitespaceOffset(this ITextSnapshotLine line) + { + var text = line.GetText(); + + for (int i = 0; i < text.Length; i++) + { + if (!char.IsWhiteSpace(text[i])) + { + return i; + } + } + + return null; + } + + /// + /// Determines whether the specified line is empty or contains whitespace only. + /// + public static bool IsEmptyOrWhitespace(this ITextSnapshotLine line) + { + var text = line.GetText(); + + for (int i = 0; i < text.Length; i++) + { + if (!char.IsWhiteSpace(text[i])) + { + return false; + } + } + + return true; + } + + public static ITextSnapshotLine GetPreviousMatchingLine(this ITextSnapshotLine line, Func predicate) + { + if (line.LineNumber <= 0) + { + return null; + } + + var snapshot = line.Snapshot; + for (int lineNumber = line.LineNumber - 1; lineNumber >= 0; lineNumber--) + { + var currentLine = snapshot.GetLineFromLineNumber(lineNumber); + if (!predicate(currentLine)) + { + continue; + } + + return currentLine; + } + + return null; + } + + public static int GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(this ITextSnapshotLine line, IEditorOptions editorOptions) + { + return line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(editorOptions.GetTabSize()); + } + + public static int GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(this ITextSnapshotLine line, int tabSize) + { + return line.GetText().GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(tabSize); + } + + public static int GetColumnFromLineOffset(this ITextSnapshotLine line, int lineOffset, IEditorOptions editorOptions) + { + return line.GetText().GetColumnFromLineOffset(lineOffset, editorOptions.GetTabSize()); + } + + public static int GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(this string line, int tabSize) + { + var firstNonWhitespaceChar = line.GetFirstNonWhitespaceOffset(); + + if (firstNonWhitespaceChar.HasValue) + { + return line.GetColumnFromLineOffset(firstNonWhitespaceChar.Value, tabSize); + } + else + { + // It's all whitespace, so go to the end + return line.GetColumnFromLineOffset(line.Length, tabSize); + } + } + + public static int? GetFirstNonWhitespaceOffset(this string line) + { + for (int i = 0; i < line.Length; i++) + { + if (!char.IsWhiteSpace(line[i])) + { + return i; + } + } + + return null; + } + + public static string GetLeadingWhitespace(this string lineText) + { + var firstOffset = lineText.GetFirstNonWhitespaceOffset(); + + return firstOffset.HasValue + ? lineText.Substring(0, firstOffset.Value) + : lineText; + } + + public static int GetColumnFromLineOffset(this string line, int endPosition, int tabSize) + { + return ConvertTabToSpace(line, tabSize, 0, endPosition); + } + + public static int ConvertTabToSpace(this string textSnippet, int tabSize, int initialColumn, int endPosition) + { + int column = initialColumn; + + // now this will calculate indentation regardless of actual content on the buffer except TAB + for (int i = 0; i < endPosition; i++) + { + if (textSnippet[i] == '\t') + { + column += tabSize - column % tabSize; + } + else + { + column++; + } + } + + return column - initialColumn; + } + + /// + /// Checks if the given line at the given snapshot index starts with the provided value. + /// + public static bool StartsWith(this ITextSnapshotLine line, int index, string value, bool ignoreCase) + { + var snapshot = line.Snapshot; + if (index + value.Length > snapshot.Length) + { + return false; + } + + for (int i = 0; i < value.Length; i++) + { + var snapshotIndex = index + i; + var actualCharacter = snapshot[snapshotIndex]; + var expectedCharacter = value[i]; + + if (ignoreCase) + { + actualCharacter = char.ToLowerInvariant(actualCharacter); + expectedCharacter = char.ToLowerInvariant(expectedCharacter); + } + + if (actualCharacter != expectedCharacter) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Editor/SmartIndent.cs b/src/Editor/SmartIndent.cs new file mode 100644 index 0000000..c3b1e22 --- /dev/null +++ b/src/Editor/SmartIndent.cs @@ -0,0 +1,53 @@ +using Cake.VisualStudio.Helpers; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + +namespace Cake.VisualStudio.Editor +{ + class SmartIndent : ISmartIndent + { + private ITextView _textView; + private readonly int _tabSize; + private readonly IEditorOptions _options; + + public SmartIndent(ITextView textView, IEditorOptions options) : this(textView) + { + _options = options; + _tabSize = _options.GetTabSize(); + } + + public SmartIndent(ITextView textView) + { + _textView = textView; + _tabSize = 4; + } + + public void Dispose() + { + } + + public int? GetDesiredIndentation(ITextSnapshotLine line) + { + var offset = 0; + var prevLine = line.GetPreviousMatchingLine(l => !string.IsNullOrWhiteSpace(l.GetText())); + if (prevLine.RequiresOffset("{")) offset += _tabSize; + if (prevLine.RequiresOffset("(")) offset += _tabSize / 2; + var prevOffset = GetPreviousOffset(prevLine); + return CalculateOffset(prevOffset, offset); + } + + private int CalculateOffset(int prevOffset, int offset) + { + var i = prevOffset + offset; + return offset == _tabSize ? i%_tabSize == 0 ? i : i - _tabSize/2 : i; + } + + private int GetPreviousOffset(ITextSnapshotLine prevLine) + { + return _options == null ? prevLine.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(_tabSize) : + prevLine.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(_options); + //return isEmpty ? 0 : prevLine.Length - 1; + } + } +} diff --git a/src/Editor/SmartIndentProvider.cs b/src/Editor/SmartIndentProvider.cs new file mode 100644 index 0000000..611da34 --- /dev/null +++ b/src/Editor/SmartIndentProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.Composition; +using Cake.VisualStudio.Helpers; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; + +namespace Cake.VisualStudio.Editor +{ + [Export(typeof(ISmartIndentProvider))] + [ContentType(Constants.CakeContentType)] + class SmartIndentProvider : ISmartIndentProvider + { + [Import] private IEditorOptionsFactoryService Factory { get; set; } + + public ISmartIndent CreateSmartIndent(ITextView textView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + return Factory == null + ? new SmartIndent(textView) + : new SmartIndent(textView, Factory.GetOptions(textView)); + } + } +} \ No newline at end of file diff --git a/src/Helpers/Extensions.cs b/src/Helpers/Extensions.cs index 0ea5353..b2e850d 100644 --- a/src/Helpers/Extensions.cs +++ b/src/Helpers/Extensions.cs @@ -3,9 +3,11 @@ // See the LICENSE file in the project root for more information. using System; +using System.Linq; using EnvDTE80; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; namespace Cake.VisualStudio.Helpers { @@ -41,5 +43,11 @@ internal static void ShowStatusBarText(this DTE2 dte, string text) if (dte?.StatusBar == null) return; dte.StatusBar.Text = text; } + + internal static bool RequiresOffset(this ITextSnapshotLine line, params string[] protectedIdentifiers) + { + var content = line.GetText().TrimEnd(); + return protectedIdentifiers.Any(i => content.EndsWith(i)); + } } } \ No newline at end of file