Skip to content

Commit

Permalink
Merge pull request #44 from agc93/feature/gh-41
Browse files Browse the repository at this point in the history
(GH-41) Added support for auto-indentation in Cake files.
  • Loading branch information
agc93 authored Feb 3, 2017
2 parents f38d9ec + ca53b73 commit e9babff
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/Cake.VisualStudio.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
<Compile Include="Configuration\ConfigurationExtensions.cs" />
<Compile Include="Configuration\ConfigurationParser.cs" />
<Compile Include="ContentType\CakeContentTypeDefinition.cs" />
<Compile Include="Editor\IndentationResult.cs" />
<Compile Include="Editor\LineExtensions.cs" />
<Compile Include="Editor\SmartIndentProvider.cs" />
<Compile Include="Editor\SmartIndent.cs" />
<Compile Include="Helpers\Constants.cs" />
<Compile Include="Helpers\Extensions.cs" />
<Compile Include="Helpers\PathHelpers.cs" />
Expand Down
40 changes: 40 additions & 0 deletions src/Editor/IndentationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Cake.VisualStudio.Editor
{
/// <summary>
/// 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.
/// </summary>
internal struct IndentationResult
{
/// <summary>
/// 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).
/// </summary>
public int BasePosition { get; }

/// <summary>
/// The number of columns the indent should be at relative to the BasePosition's column.
/// </summary>
public int Offset { get; }

public IndentationResult(int basePosition, int offset) : this()
{
this.BasePosition = basePosition;
this.Offset = offset;
}
}
}
204 changes: 204 additions & 0 deletions src/Editor/LineExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Returns the first non-whitespace position on the given line, or null if
/// the line is empty or contains only whitespace.
/// </summary>
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;
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
/// Determines whether the specified line is empty or contains whitespace only.
/// </summary>
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<ITextSnapshotLine, bool> 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;
}

/// <summary>
/// Checks if the given line at the given snapshot index starts with the provided value.
/// </summary>
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;
}
}
}
53 changes: 53 additions & 0 deletions src/Editor/SmartIndent.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
27 changes: 27 additions & 0 deletions src/Editor/SmartIndentProvider.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
8 changes: 8 additions & 0 deletions src/Helpers/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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));
}
}
}

0 comments on commit e9babff

Please sign in to comment.