aspnetcore/src/Microsoft.VisualStudio.Edit.../BraceSmartIndenter.cs

260 lines
9.4 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
namespace Microsoft.VisualStudio.Editor.Razor
{
/// <summary>
/// This class is responsible for handling situations where Roslyn and the HTML editor cannot auto-indent Razor code.
/// </summary>
/// <example>
/// Attempting to insert a newline (pipe indicates the cursor):
/// @{ |}
/// Should result in the text buffer looking like the following:
/// @{
/// |
/// }
/// This is also true for directive block scenarios.
/// </example>
internal class BraceSmartIndenter : IDisposable
{
private readonly ForegroundDispatcher _dispatcher;
private readonly ITextBuffer _textBuffer;
private readonly VisualStudioDocumentTracker _documentTracker;
private readonly IEditorOperationsFactoryService _editorOperationsFactory;
private readonly StringBuilder _indentBuilder = new StringBuilder();
private BraceIndentationContext _context;
// Internal for testing
internal BraceSmartIndenter()
{
}
public BraceSmartIndenter(
ForegroundDispatcher dispatcher,
VisualStudioDocumentTracker documentTracker,
IEditorOperationsFactoryService editorOperationsFactory)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
if (documentTracker == null)
{
throw new ArgumentNullException(nameof(documentTracker));
}
if (editorOperationsFactory == null)
{
throw new ArgumentNullException(nameof(editorOperationsFactory));
}
_dispatcher = dispatcher;
_documentTracker = documentTracker;
_editorOperationsFactory = editorOperationsFactory;
_textBuffer = _documentTracker.TextBuffer;
_textBuffer.Changed += TextBuffer_OnChanged;
_textBuffer.PostChanged += TextBuffer_OnPostChanged;
}
public void Dispose()
{
_dispatcher.AssertForegroundThread();
_textBuffer.Changed -= TextBuffer_OnChanged;
_textBuffer.PostChanged -= TextBuffer_OnPostChanged;
}
// Internal for testing
internal void TriggerSmartIndent(ITextView textView)
{
// This forces the smart indent. For example attempting to enter a newline between the functions directive:
// @functions {} will not auto-indent in between the braces unless we forcefully move to end of line.
var editorOperations = _editorOperationsFactory.GetEditorOperations(textView);
editorOperations.MoveToEndOfLine(false);
}
// Internal for testing
internal void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs args)
{
_dispatcher.AssertForegroundThread();
if (!args.TextChangeOccurred(out var changeInformation))
{
return;
}
var newText = changeInformation.newText;
if (TryCreateIndentationContext(changeInformation.firstChange.NewPosition, newText.Length, newText, _documentTracker, out var context))
{
_context = context;
}
}
private void TextBuffer_OnPostChanged(object sender, EventArgs e)
{
_dispatcher.AssertForegroundThread();
var context = _context;
_context = null;
if (context != null)
{
// Save the current caret position
var textView = context.FocusedTextView;
var caret = textView.Caret.Position.BufferPosition;
var textViewBuffer = textView.TextBuffer;
var indent = CalculateIndent(textViewBuffer, context.ChangePosition);
// Current state, pipe is cursor:
// @{
// |}
// Insert the completion text, i.e. "\r\n "
InsertIndent(caret.Position, indent, textViewBuffer);
// @{
//
// |}
// Place the caret inbetween the braces (before our indent).
RestoreCaretTo(caret.Position, textView);
// @{
// |
// }
// For Razor metacode cases the editor's smart indent wont kick in automatically.
TriggerSmartIndent(textView);
// @{
// |
// }
}
}
private string CalculateIndent(ITextBuffer buffer, int from)
{
// Get the line text of the block start
var currentSnapshotPoint = new SnapshotPoint(buffer.CurrentSnapshot, from);
var line = buffer.CurrentSnapshot.GetLineFromPosition(currentSnapshotPoint);
var lineText = line.GetText();
// Gather up the indent from the start block
_indentBuilder.Append(line.GetLineBreakText());
foreach (var ch in lineText)
{
if (!char.IsWhiteSpace(ch))
{
break;
}
_indentBuilder.Append(ch);
}
var indent = _indentBuilder.ToString();
_indentBuilder.Clear();
return indent;
}
// Internal for testing
internal static void InsertIndent(int insertLocation, string indent, ITextBuffer textBuffer)
{
var edit = textBuffer.CreateEdit();
edit.Insert(insertLocation, indent);
edit.Apply();
}
// Internal for testing
internal static void RestoreCaretTo(int caretPosition, ITextView textView)
{
var currentSnapshotPoint = new SnapshotPoint(textView.TextBuffer.CurrentSnapshot, caretPosition);
textView.Caret.MoveTo(currentSnapshotPoint);
}
// Internal for testing
internal static bool TryCreateIndentationContext(int changePosition, int changeLength, string finalText, VisualStudioDocumentTracker documentTracker, out BraceIndentationContext context)
{
var focusedTextView = documentTracker.GetFocusedTextView();
if (focusedTextView != null && ParserHelpers.IsNewLine(finalText))
{
var currentSnapshot = documentTracker.TextBuffer.CurrentSnapshot;
var preChangeLineSnapshot = currentSnapshot.GetLineFromPosition(changePosition);
// Handle the case where the \n comes through separately from the \r and the position
// on the line is beyond what the GetText call above gives back.
var linePosition = Math.Min(preChangeLineSnapshot.Length, changePosition - preChangeLineSnapshot.Start) - 1;
if (AfterOpeningBrace(linePosition, preChangeLineSnapshot))
{
var afterChangePosition = changePosition + changeLength;
var afterChangeLineSnapshot = currentSnapshot.GetLineFromPosition(afterChangePosition);
var afterChangeLinePosition = afterChangePosition - afterChangeLineSnapshot.Start;
if (BeforeClosingBrace(afterChangeLinePosition, afterChangeLineSnapshot))
{
context = new BraceIndentationContext(focusedTextView, changePosition);
return true;
}
}
}
context = null;
return false;
}
internal static bool BeforeClosingBrace(int linePosition, ITextSnapshotLine lineSnapshot)
{
var lineText = lineSnapshot.GetText();
for (; linePosition < lineSnapshot.Length; linePosition++)
{
if (!char.IsWhiteSpace(lineText[linePosition]))
{
break;
}
}
var beforeClosingBrace = linePosition < lineSnapshot.Length && lineText[linePosition] == '}';
return beforeClosingBrace;
}
internal static bool AfterOpeningBrace(int linePosition, ITextSnapshotLine lineSnapshot)
{
var lineText = lineSnapshot.GetText();
for (; linePosition >= 0; linePosition--)
{
if (!char.IsWhiteSpace(lineText[linePosition]))
{
break;
}
}
var afterClosingBrace = linePosition >= 0 && lineText[linePosition] == '{';
return afterClosingBrace;
}
internal class BraceIndentationContext
{
public BraceIndentationContext(ITextView focusedTextView, int changePosition)
{
FocusedTextView = focusedTextView;
ChangePosition = changePosition;
}
public ITextView FocusedTextView { get; }
public int ChangePosition { get; }
}
}
}