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