diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserHelpers.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserHelpers.cs
index 0ad2263a4a..71835178b2 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserHelpers.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserHelpers.cs
@@ -10,13 +10,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal static class ParserHelpers
{
+ public static char[] NewLineCharacters = new[]
+ {
+ '\r', // Carriage return
+ '\n', // Linefeed
+ '\u0085', // Next Line
+ '\u2028', // Line separator
+ '\u2029' // Paragraph separator
+ };
+
public static bool IsNewLine(char value)
{
- return value == '\r' // Carriage return
- || value == '\n' // Linefeed
- || value == '\u0085' // Next Line
- || value == '\u2028' // Line separator
- || value == '\u2029'; // Paragraph separator
+ return NewLineCharacters.Contains(value);
}
public static bool IsNewLine(string value)
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs
index c3e9d48681..2e1dafb7a8 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/AssemblyInfo.cs
@@ -12,6 +12,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor.Test.Common, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs
new file mode 100644
index 0000000000..635f3d18e6
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs
@@ -0,0 +1,268 @@
+// 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
+{
+ ///
+ /// This class is responsible for handling situations where Roslyn and the HTML editor cannot auto-indent Razor code.
+ ///
+ ///
+ /// 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.
+ ///
+ internal class BraceSmartIndenter : IDisposable
+ {
+ private readonly ForegroundDispatcher _dispatcher;
+ private readonly ITextBuffer _textBuffer;
+ private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory;
+ private readonly IEditorOperationsFactoryService _editorOperationsFactory;
+ private readonly StringBuilder _indentBuilder = new StringBuilder();
+ private BraceIndentationContext _context;
+
+ public BraceSmartIndenter(
+ ForegroundDispatcher dispatcher,
+ ITextBuffer textBuffer,
+ VisualStudioDocumentTrackerFactory documentTrackerFactory,
+ IEditorOperationsFactoryService editorOperationsFactory)
+ {
+ if (dispatcher == null)
+ {
+ throw new ArgumentNullException(nameof(dispatcher));
+ }
+
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException(nameof(textBuffer));
+ }
+
+ if (documentTrackerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(documentTrackerFactory));
+ }
+
+ if (editorOperationsFactory == null)
+ {
+ throw new ArgumentNullException(nameof(editorOperationsFactory));
+ }
+
+ _dispatcher = dispatcher;
+ _textBuffer = textBuffer;
+ _documentTrackerFactory = documentTrackerFactory;
+ _editorOperationsFactory = editorOperationsFactory;
+ _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 documentTracker = _documentTrackerFactory.GetTracker(_textBuffer);
+
+ // Extra hardening, this should never be null.
+ if (documentTracker == null)
+ {
+ 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; }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Properties/Resources.Designer.cs b/src/Microsoft.VisualStudio.Editor.Razor/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..922d049da8
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Editor.Razor/Properties/Resources.Designer.cs
@@ -0,0 +1,44 @@
+//
+namespace Microsoft.VisualStudio.Editor.Razor
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.VisualStudio.Editor.Razor.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// Value cannot be null or an empty string.
+ ///
+ internal static string ArgumentCannotBeNullOrEmpty
+ {
+ get => GetString("ArgumentCannotBeNullOrEmpty");
+ }
+
+ ///
+ /// Value cannot be null or an empty string.
+ ///
+ internal static string FormatArgumentCannotBeNullOrEmpty()
+ => GetString("ArgumentCannotBeNullOrEmpty");
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Resources.resx b/src/Microsoft.VisualStudio.Editor.Razor/Resources.resx
new file mode 100644
index 0000000000..55a253ebbe
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Editor.Razor/Resources.resx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Value cannot be null or an empty string.
+
+
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/TextContentChangedEventArgsExtensions.cs b/src/Microsoft.VisualStudio.Editor.Razor/TextContentChangedEventArgsExtensions.cs
new file mode 100644
index 0000000000..2e274ee885
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Editor.Razor/TextContentChangedEventArgsExtensions.cs
@@ -0,0 +1,38 @@
+// 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;
+
+namespace Microsoft.VisualStudio.Text
+{
+ internal static class TextContentChangedEventArgsExtensions
+ {
+ public static bool TextChangeOccurred(this TextContentChangedEventArgs args, out (ITextChange firstChange, ITextChange lastChange, string newText, string oldText) changeInformation)
+ {
+ if (args.Changes.Count > 0)
+ {
+ var firstChange = args.Changes[0];
+ var lastChange = args.Changes[args.Changes.Count - 1];
+ var oldLength = lastChange.OldEnd - firstChange.OldPosition;
+ var newLength = lastChange.NewEnd - firstChange.NewPosition;
+ var newText = args.After.GetText(firstChange.NewPosition, newLength);
+ var oldText = args.Before.GetText(firstChange.OldPosition, oldLength);
+
+ var wasChanged = true;
+ if (oldLength == newLength)
+ {
+ wasChanged = !string.Equals(oldText, newText, StringComparison.Ordinal);
+ }
+
+ if (wasChanged)
+ {
+ changeInformation = (firstChange, lastChange, newText, oldText);
+ return true;
+ }
+ }
+
+ changeInformation = default((ITextChange, ITextChange, string, string));
+ return false;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs
index 48abb122da..2c4c6858d6 100644
--- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs
+++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs
@@ -25,5 +25,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract ITextBuffer TextBuffer { get; }
public abstract IReadOnlyList TextViews { get; }
+
+ public abstract ITextView GetFocusedTextView();
}
}
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs
index 60f3b8e7b4..7795f83ddc 100644
--- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs
+++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.Editor.Razor
@@ -8,5 +9,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract class VisualStudioDocumentTrackerFactory
{
public abstract VisualStudioDocumentTracker GetTracker(ITextView textView);
+
+ public abstract VisualStudioDocumentTracker GetTracker(ITextBuffer textBuffer);
}
}
diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs
index b101361cf1..c0e75084f7 100644
--- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs
+++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs
@@ -2,15 +2,17 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Timers;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
-using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Operations;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
-using Timer = System.Timers.Timer;
+using Timer = System.Threading.Timer;
namespace Microsoft.VisualStudio.Editor.Razor
{
@@ -18,13 +20,17 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Internal for testing.
internal readonly ITextBuffer _textBuffer;
- internal readonly Timer _idleTimer;
+ internal TimeSpan IdleDelay = TimeSpan.FromSeconds(3);
+ internal Timer _idleTimer;
- private const int IdleDelay = 3000;
+ private readonly object IdleLock = new object();
private readonly ICompletionBroker _completionBroker;
+ private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory;
private readonly BackgroundParser _parser;
private readonly ForegroundDispatcher _dispatcher;
+ private readonly ErrorReporter _errorReporter;
private RazorSyntaxTreePartialParser _partialParser;
+ private BraceSmartIndenter _braceSmartIndenter;
// For testing only
internal VisualStudioRazorParser(RazorCodeDocument codeDocument)
@@ -32,7 +38,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
CodeDocument = codeDocument;
}
- public VisualStudioRazorParser(ForegroundDispatcher dispatcher, ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker)
+ public VisualStudioRazorParser(
+ ForegroundDispatcher dispatcher,
+ ITextBuffer buffer,
+ RazorTemplateEngine templateEngine,
+ string filePath,
+ ErrorReporter errorReporter,
+ ICompletionBroker completionBroker,
+ VisualStudioDocumentTrackerFactory documentTrackerFactory,
+ IEditorOperationsFactoryService editorOperationsFactory)
{
if (dispatcher == null)
{
@@ -54,20 +68,36 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}
+ if (errorReporter == null)
+ {
+ throw new ArgumentNullException(nameof(errorReporter));
+ }
+
if (completionBroker == null)
{
throw new ArgumentNullException(nameof(completionBroker));
}
+ if (documentTrackerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(documentTrackerFactory));
+ }
+
+ if (editorOperationsFactory == null)
+ {
+ throw new ArgumentNullException(nameof(editorOperationsFactory));
+ }
+
_dispatcher = dispatcher;
TemplateEngine = templateEngine;
FilePath = filePath;
+ _errorReporter = errorReporter;
_textBuffer = buffer;
_completionBroker = completionBroker;
+ _documentTrackerFactory = documentTrackerFactory;
_textBuffer.Changed += TextBuffer_OnChanged;
+ _braceSmartIndenter = new BraceSmartIndenter(_dispatcher, _textBuffer, _documentTrackerFactory, editorOperationsFactory);
_parser = new BackgroundParser(templateEngine, filePath);
- _idleTimer = new Timer(IdleDelay);
- _idleTimer.Elapsed += Onidle;
_parser.ResultsReady += OnResultsReady;
_parser.Start();
@@ -95,83 +125,126 @@ namespace Microsoft.VisualStudio.Editor.Razor
_dispatcher.AssertForegroundThread();
_textBuffer.Changed -= TextBuffer_OnChanged;
+ _braceSmartIndenter.Dispose();
_parser.Dispose();
- _idleTimer.Dispose();
+
+ StopIdleTimer();
}
- private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs contentChange)
+ // Internal for testing
+ internal void StartIdleTimer()
{
_dispatcher.AssertForegroundThread();
- if (contentChange.Changes.Count > 0)
+ lock (IdleLock)
{
- // Idle timers are used to track provisional changes. Provisional changes only last for a single text change. After that normal
- // partial parsing rules apply (stop the timer).
- _idleTimer.Stop();
-
-
- var firstChange = contentChange.Changes[0];
- var lastChange = contentChange.Changes[contentChange.Changes.Count - 1];
-
- var oldLen = lastChange.OldEnd - firstChange.OldPosition;
- var newLen = lastChange.NewEnd - firstChange.NewPosition;
-
- var wasChanged = true;
- if (oldLen == newLen)
+ if (_idleTimer == null)
{
- var oldText = contentChange.Before.GetText(firstChange.OldPosition, oldLen);
- var newText = contentChange.After.GetText(firstChange.NewPosition, newLen);
- wasChanged = !string.Equals(oldText, newText, StringComparison.Ordinal);
- }
-
- if (wasChanged)
- {
- var newText = contentChange.After.GetText(firstChange.NewPosition, newLen);
- var change = new SourceChange(firstChange.OldPosition, oldLen, newText);
- var snapshot = contentChange.After;
- var result = PartialParseResultInternal.Rejected;
-
- using (_parser.SynchronizeMainThreadState())
- {
- // Check if we can partial-parse
- if (_partialParser != null && _parser.IsIdle)
- {
- result = _partialParser.Parse(change);
- }
- }
-
- // If partial parsing failed or there were outstanding parser tasks, start a full reparse
- if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
- {
- _parser.QueueChange(change, snapshot);
- }
-
- if ((result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional)
- {
- _idleTimer.Start();
- }
+ // Timer will fire after a fixed delay, but only once.
+ _idleTimer = new Timer(Timer_Tick, null, IdleDelay, Timeout.InfiniteTimeSpan);
}
}
}
- private void Onidle(object sender, ElapsedEventArgs e)
+ // Internal for testing
+ internal void StopIdleTimer()
{
- _dispatcher.AssertBackgroundThread();
+ // Can be called from any thread.
- var textViews = Array.Empty();
+ lock (IdleLock)
+ {
+ if (_idleTimer != null)
+ {
+ _idleTimer.Dispose();
+ _idleTimer = null;
+ }
+ }
+ }
- foreach (var textView in textViews)
+ private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs args)
+ {
+ _dispatcher.AssertForegroundThread();
+
+ if (args.Changes.Count > 0)
+ {
+ // Idle timers are used to track provisional changes. Provisional changes only last for a single text change. After that normal
+ // partial parsing rules apply (stop the timer).
+ StopIdleTimer();
+ }
+
+ if (!args.TextChangeOccurred(out var changeInformation))
+ {
+ return;
+ }
+
+ var change = new SourceChange(changeInformation.firstChange.OldPosition, changeInformation.oldText.Length, changeInformation.newText);
+ var snapshot = args.After;
+ var result = PartialParseResultInternal.Rejected;
+
+ using (_parser.SynchronizeMainThreadState())
+ {
+ // Check if we can partial-parse
+ if (_partialParser != null && _parser.IsIdle)
+ {
+ result = _partialParser.Parse(change);
+ }
+ }
+
+ // If partial parsing failed or there were outstanding parser tasks, start a full reparse
+ if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
+ {
+ _parser.QueueChange(change, snapshot);
+ }
+
+ if ((result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional)
+ {
+ StartIdleTimer();
+ }
+ }
+
+ private void OnIdle(object state)
+ {
+ _dispatcher.AssertForegroundThread();
+
+ var documentTracker = _documentTrackerFactory.GetTracker(_textBuffer);
+
+ if (documentTracker == null)
+ {
+ Debug.Fail("Document tracker should never be null when checking idle state.");
+ return;
+ }
+
+ foreach (var textView in documentTracker.TextViews)
{
if (_completionBroker.IsCompletionActive(textView))
{
+ // Completion list is still active, need to re-start timer.
+ StartIdleTimer();
return;
}
}
- _idleTimer.Stop();
Reparse();
}
+ private async void Timer_Tick(object state)
+ {
+ try
+ {
+ _dispatcher.AssertBackgroundThread();
+
+ StopIdleTimer();
+
+ // We need to get back to the UI thread to properly check if a completion is active.
+ await Task.Factory.StartNew(OnIdle, null, CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
+ }
+ catch (Exception ex)
+ {
+ // This is something totally unexpected, let's just send it over to the workspace.
+ await Task.Factory.StartNew(() => _errorReporter.ReportError(ex), CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
+ }
+ }
+
private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
{
_dispatcher.AssertBackgroundThread();
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs
index b852a11e48..a83d7e769c 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs
@@ -79,6 +79,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public override Workspace Workspace => _workspace;
+ public override ITextView GetFocusedTextView()
+ {
+ for (var i = 0; i < TextViews.Count; i++)
+ {
+ if (TextViews[i].HasAggregateFocus)
+ {
+ return TextViews[i];
+ }
+ }
+
+ return null;
+ }
+
public void Subscribe()
{
// Fundamentally we have a Razor half of the world as as soon as the document is open - and then later
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs
index 8119556d04..2ed1a1ad23 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs
@@ -114,6 +114,31 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return tracker;
}
+ public override VisualStudioDocumentTracker GetTracker(ITextBuffer textBuffer)
+ {
+ if (textBuffer == null)
+ {
+ throw new ArgumentNullException(nameof(textBuffer));
+ }
+
+ _foregroundDispatcher.AssertForegroundThread();
+
+ if (!textBuffer.IsRazorBuffer())
+ {
+ // Not a Razor buffer.
+ return null;
+ }
+
+ // A little bit of hardening here, to make sure our assumptions are correct.
+ DefaultVisualStudioDocumentTracker tracker;
+ if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
+ {
+ Debug.Fail("The document tracker should be initialized");
+ }
+
+ return tracker;
+ }
+
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection subjectBuffers)
{
if (textView == null)
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/StringTextSnapshot.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/StringTextSnapshot.cs
index 5072f89ccd..16295be713 100644
--- a/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/StringTextSnapshot.cs
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/StringTextSnapshot.cs
@@ -4,15 +4,41 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Text
{
public class StringTextSnapshot : ITextSnapshot
{
+ private readonly List _lines;
+
public StringTextSnapshot(string content)
{
Content = content;
+ _lines = new List();
+
+ var start = 0;
+ var delimiterIndex = 0;
+ while (delimiterIndex != -1)
+ {
+ var delimiterLength = 2;
+ delimiterIndex = Content.IndexOf("\r\n", start);
+
+ if (delimiterIndex == -1)
+ {
+ delimiterLength = 1;
+ delimiterIndex = Content.IndexOfAny(ParserHelpers.NewLineCharacters, start);
+ }
+
+ var nextLineStartIndex = delimiterIndex != -1 ? delimiterIndex + delimiterLength : Content.Length;
+
+ var lineText = Content.Substring(start, nextLineStartIndex - start);
+ _lines.Add(new SnapshotLine(lineText, start, this));
+
+ start = nextLineStartIndex;
+ }
}
public string Content { get; }
@@ -23,7 +49,7 @@ namespace Microsoft.VisualStudio.Text
public int Length => Content.Length;
- public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
+ public ITextBuffer TextBuffer => throw new NotImplementedException();
public IContentType ContentType => throw new NotImplementedException();
@@ -39,6 +65,18 @@ namespace Microsoft.VisualStudio.Text
public char[] ToCharArray(int startIndex, int length) => Content.ToCharArray();
+ public ITextSnapshotLine GetLineFromPosition(int position)
+ {
+ var matchingLine = _lines.FirstOrDefault(line => line.Start + line.LengthIncludingLineBreak > position);
+
+ if (position < 0 || matchingLine == null)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return matchingLine;
+ }
+
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) => throw new NotImplementedException();
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
@@ -53,8 +91,6 @@ namespace Microsoft.VisualStudio.Text
public ITextSnapshotLine GetLineFromLineNumber(int lineNumber) => throw new NotImplementedException();
- public ITextSnapshotLine GetLineFromPosition(int position) => throw new NotImplementedException();
-
public int GetLineNumberFromPosition(int position) => throw new NotImplementedException();
public string GetText(VisualStudio.Text.Span span) => throw new NotImplementedException();
@@ -71,7 +107,7 @@ namespace Microsoft.VisualStudio.Text
public int Length => throw new NotImplementedException();
- public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
+ public ITextBuffer TextBuffer => throw new NotImplementedException();
public int VersionNumber => throw new NotImplementedException();
@@ -96,5 +132,55 @@ namespace Microsoft.VisualStudio.Text
public bool IncludesLineChanges => false;
}
}
+
+ private class SnapshotLine : ITextSnapshotLine
+ {
+ private readonly string _contentWithLineBreak;
+ private readonly string _content;
+
+ public SnapshotLine(string contentWithLineBreak, int start, ITextSnapshot owner)
+ {
+ _contentWithLineBreak = contentWithLineBreak;
+ _content = contentWithLineBreak;
+
+ if (_content.EndsWith("\r\n"))
+ {
+ _content = _content.Substring(0, _content.Length - 2);
+ }
+ else if(_content.Length > 0 && ParserHelpers.NewLineCharacters.Contains(_content[_content.Length - 1]))
+ {
+ _content = _content.Substring(0, _content.Length - 1);
+ }
+
+ Start = new SnapshotPoint(owner, start);
+ Snapshot = owner;
+ }
+
+ public ITextSnapshot Snapshot { get; }
+
+ public SnapshotPoint Start { get; }
+
+ public int Length => _content.Length;
+
+ public int LengthIncludingLineBreak => _contentWithLineBreak.Length;
+
+ public int LineBreakLength => _contentWithLineBreak.Length - _content.Length;
+
+ public string GetText() => _content;
+
+ public string GetLineBreakText() => _contentWithLineBreak.Substring(_content.Length);
+
+ public string GetTextIncludingLineBreak() => _contentWithLineBreak;
+
+ public int LineNumber => throw new NotImplementedException();
+
+ public SnapshotSpan Extent => throw new NotImplementedException();
+
+ public SnapshotSpan ExtentIncludingLineBreak => throw new NotImplementedException();
+
+ public SnapshotPoint End => throw new NotImplementedException();
+
+ public SnapshotPoint EndIncludingLineBreak => throw new NotImplementedException();
+ }
}
}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs
new file mode 100644
index 0000000000..0287895386
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs
@@ -0,0 +1,88 @@
+// 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 Microsoft.VisualStudio.Test;
+using Microsoft.VisualStudio.Text;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Editor.Razor
+{
+ public class BraceSmartIndenterIntegrationTest : BraceSmartIndenterTestBase
+ {
+ [ForegroundFact]
+ public void TextBuffer_OnPostChanged_IndentsInbetweenBraces_BaseIndentation()
+ {
+ // Arrange
+ var change = Environment.NewLine;
+ var initialSnapshot = new StringTextSnapshot("@{ }");
+ var afterChangeSnapshot = new StringTextSnapshot("@{ " + change + "}");
+ var edit = new TestEdit(3, 0, initialSnapshot, change.Length, afterChangeSnapshot, change);
+ var expectedIndentResult = "@{ " + change + change + "}";
+
+ var caret = CreateCaretFrom(3 + change.Length, afterChangeSnapshot);
+ TestTextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer, caret);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+ var editorOperationsFactory = CreateOperationsFactoryService();
+ var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
+
+ // Act
+ textBuffer.ApplyEdit(edit);
+
+ // Assert
+ Assert.Equal(expectedIndentResult, ((StringTextSnapshot)textBuffer.CurrentSnapshot).Content);
+ }
+
+ [ForegroundFact]
+ public void TextBuffer_OnPostChanged_IndentsInbetweenBraces_OneLevelOfIndentation()
+ {
+ // Arrange
+ var change = "\r";
+ var initialSnapshot = new StringTextSnapshot(" @{ }");
+ var afterChangeSnapshot = new StringTextSnapshot(" @{ " + change + "}");
+ var edit = new TestEdit(7, 0, initialSnapshot, change.Length, afterChangeSnapshot, change);
+ var expectedIndentResult = " @{ " + change + change + " }";
+
+ var caret = CreateCaretFrom(7 + change.Length, afterChangeSnapshot);
+ TestTextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer, caret);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+ var editorOperationsFactory = CreateOperationsFactoryService();
+ var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
+
+ // Act
+ textBuffer.ApplyEdit(edit);
+
+ // Assert
+ Assert.Equal(expectedIndentResult, ((StringTextSnapshot)textBuffer.CurrentSnapshot).Content);
+ }
+
+ [ForegroundFact]
+ public void TextBuffer_OnPostChanged_IndentsInbetweenDirectiveBlockBraces()
+ {
+ // Arrange
+ var change = Environment.NewLine;
+ var initialSnapshot = new StringTextSnapshot(" @functions {}");
+ var afterChangeSnapshot = new StringTextSnapshot(" @functions {" + change + "}");
+ var edit = new TestEdit(16, 0, initialSnapshot, change.Length, afterChangeSnapshot, change);
+ var expectedIndentResult = " @functions {" + change + change + " }";
+
+ var caret = CreateCaretFrom(16 + change.Length, afterChangeSnapshot);
+ TestTextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer, caret);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+ var editorOperationsFactory = CreateOperationsFactoryService();
+ var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
+
+ // Act
+ textBuffer.ApplyEdit(edit);
+
+ // Assert
+ Assert.Equal(expectedIndentResult, ((StringTextSnapshot)textBuffer.CurrentSnapshot).Content);
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs
new file mode 100644
index 0000000000..5fa523db88
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs
@@ -0,0 +1,299 @@
+// 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.Collections.Generic;
+using Microsoft.VisualStudio.Test;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Operations;
+using Moq;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Editor.Razor
+{
+ public class BraceSmartIndenterTest : BraceSmartIndenterTestBase
+ {
+ [Fact]
+ public void InsertIndent_InsertsProvidedIndentIntoBuffer()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("@{ \n}");
+ var expectedIndentResult = "@{ anything\n}";
+ ITextBuffer textBuffer = null;
+ var textView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, textView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ BraceSmartIndenter.InsertIndent(3, "anything", textBuffer);
+
+ // Assert
+ Assert.Equal(expectedIndentResult, ((StringTextSnapshot)textBuffer.CurrentSnapshot).Content);
+ }
+
+ [Fact]
+ public void RestoreCaretTo_PlacesCursorAtProvidedPosition()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("@{ \n\n}");
+ var bufferPosition = new VirtualSnapshotPoint(initialSnapshot, 4);
+ var caret = new Mock();
+ caret.Setup(c => c.MoveTo(It.IsAny()))
+ .Callback(point =>
+ {
+ Assert.Equal(3, point.Position);
+ Assert.Same(initialSnapshot, point.Snapshot);
+ });
+ ITextBuffer textBuffer = null;
+ var textView = CreateFocusedTextView(() => textBuffer, caret.Object);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, textView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ BraceSmartIndenter.RestoreCaretTo(3, textView);
+
+ // Assert
+ caret.VerifyAll();
+ }
+
+ [Fact]
+ public void TriggerSmartIndent_ForcesEditorToMoveToEndOfLine()
+ {
+ // Arrange
+ var textView = CreateFocusedTextView();
+ var editorOperations = new Mock();
+ editorOperations.Setup(operations => operations.MoveToEndOfLine(false));
+ var editorOperationsFactory = new Mock();
+ editorOperationsFactory.Setup(factory => factory.GetEditorOperations(textView))
+ .Returns(editorOperations.Object);
+ var smartIndenter = new BraceSmartIndenter(Dispatcher, new Mock().Object, new Mock().Object, editorOperationsFactory.Object);
+
+ // Act
+ smartIndenter.TriggerSmartIndent(textView);
+
+ // Assert
+ editorOperations.VerifyAll();
+ }
+
+ [Fact]
+ public void AfterClosingBrace_ContentAfterBrace_ReturnsFalse()
+ {
+ // Arrange
+ var fileSnapshot = new StringTextSnapshot("@functions\n{a\n}");
+ var changePosition = 13;
+ var line = fileSnapshot.GetLineFromPosition(changePosition);
+
+ // Act & Assert
+ Assert.False(BraceSmartIndenter.BeforeClosingBrace(0, line));
+ }
+
+ [Theory]
+ [InlineData("@functions\n{\n}")]
+ [InlineData("@functions\n{ \n}")]
+ [InlineData("@functions\n { \n}")]
+ [InlineData("@functions\n\t\t{\t\t\n}")]
+ public void AfterClosingBrace_BraceBeforePosition_ReturnsTrue(string fileContent)
+ {
+ // Arrange
+ var fileSnapshot = new StringTextSnapshot(fileContent);
+ var changePosition = fileContent.Length - 3 /* \n} */;
+ var line = fileSnapshot.GetLineFromPosition(changePosition);
+
+ // Act & Assert
+ Assert.True(BraceSmartIndenter.AfterOpeningBrace(line.Length - 1, line));
+ }
+
+ [Fact]
+ public void BeforeClosingBrace_ContentPriorToBrace_ReturnsFalse()
+ {
+ // Arrange
+ var fileSnapshot = new StringTextSnapshot("@functions\n{\na}");
+ var changePosition = 12;
+ var line = fileSnapshot.GetLineFromPosition(changePosition + 1 /* \n */);
+
+ // Act & Assert
+ Assert.False(BraceSmartIndenter.BeforeClosingBrace(0, line));
+ }
+
+ [Theory]
+ [InlineData("@functions\n{\n}")]
+ [InlineData("@functions\n{\n }")]
+ [InlineData("@functions\n{\n } ")]
+ [InlineData("@functions\n{\n\t\t } ")]
+ public void BeforeClosingBrace_BraceAfterPosition_ReturnsTrue(string fileContent)
+ {
+ // Arrange
+ var fileSnapshot = new StringTextSnapshot(fileContent);
+ var changePosition = 12;
+ var line = fileSnapshot.GetLineFromPosition(changePosition + 1 /* \n */);
+
+ // Act & Assert
+ Assert.True(BraceSmartIndenter.BeforeClosingBrace(0, line));
+ }
+
+ [ForegroundFact]
+ public void TextBuffer_OnChanged_NoopsIfNoChanges()
+ {
+ // Arrange
+ var textBuffer = new Mock();
+ var editorOperationsFactory = new Mock();
+ var changeCollection = new TestTextChangeCollection();
+ var textContentChangeArgs = new TestTextContentChangedEventArgs(changeCollection);
+ var documentTrackerFactory = new Mock();
+ var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer.Object, documentTrackerFactory.Object, editorOperationsFactory.Object);
+
+ // Act & Assert
+ braceSmartIndenter.TextBuffer_OnChanged(null, textContentChangeArgs);
+ }
+
+ [ForegroundFact]
+ public void TextBuffer_OnChanged_NoopsIfChangesThatResultInNoChange()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("Hello World");
+ var textBuffer = new TestTextBuffer(initialSnapshot);
+ var edit = new TestEdit(0, 0, initialSnapshot, 0, initialSnapshot, string.Empty);
+ var editorOperationsFactory = new Mock();
+ var documentTrackerFactory = new Mock();
+ var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, documentTrackerFactory.Object, editorOperationsFactory.Object);
+
+ // Act & Assert
+ textBuffer.ApplyEdits(edit, edit);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsFalseIfNoFocusedTextView()
+ {
+ // Arrange
+ var snapshot = new StringTextSnapshot(Environment.NewLine + "Hello World");
+ ITextBuffer textBuffer = null;
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView: null);
+ textBuffer = CreateTextBuffer(snapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(0, Environment.NewLine.Length, Environment.NewLine, documentTracker, out var context);
+
+ // Assert
+ Assert.Null(context);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsFalseIfTextChangeIsNotNewline()
+ {
+ // Arrange
+ var snapshot = new StringTextSnapshot("This Hello World");
+ ITextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(snapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(0, 5, "This ", documentTracker, out var context);
+
+ // Assert
+ Assert.Null(context);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsFalseIfNewLineIsNotPrecededByOpenBrace_FileStart()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot(Environment.NewLine + "Hello World");
+ ITextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(0, Environment.NewLine.Length, Environment.NewLine, documentTracker, out var context);
+
+ // Assert
+ Assert.Null(context);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsFalseIfNewLineIsNotPrecededByOpenBrace_MidFile()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("Hello\u0085World");
+ ITextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(5, 1, "\u0085", documentTracker, out var context);
+
+ // Assert
+ Assert.Null(context);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsFalseIfNewLineIsNotFollowedByCloseBrace()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("@{ " + Environment.NewLine + "World");
+ ITextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(3, Environment.NewLine.Length, Environment.NewLine, documentTracker, out var context);
+
+ // Assert
+ Assert.Null(context);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TryCreateIndentationContext_ReturnsTrueIfNewLineIsSurroundedByBraces()
+ {
+ // Arrange
+ var initialSnapshot = new StringTextSnapshot("@{ \n}");
+ ITextBuffer textBuffer = null;
+ var focusedTextView = CreateFocusedTextView(() => textBuffer);
+ var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
+ textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
+
+ // Act
+ var result = BraceSmartIndenter.TryCreateIndentationContext(3, 1, "\n", documentTracker, out var context);
+
+ // Assert
+ Assert.NotNull(context);
+ Assert.Same(focusedTextView, context.FocusedTextView);
+ Assert.Equal(3, context.ChangePosition);
+ Assert.True(result);
+ }
+
+ protected class TestTextContentChangedEventArgs : TextContentChangedEventArgs
+ {
+ public TestTextContentChangedEventArgs(INormalizedTextChangeCollection changeCollection)
+ : base(CreateBeforeSnapshot(changeCollection), new Mock().Object, EditOptions.DefaultMinimalChange, null)
+ {
+ }
+
+ protected static ITextSnapshot CreateBeforeSnapshot(INormalizedTextChangeCollection collection)
+ {
+ var version = new Mock();
+ version.Setup(v => v.Changes)
+ .Returns(collection);
+ var snapshot = new Mock();
+ snapshot.Setup(obj => obj.Version)
+ .Returns(version.Object);
+
+ return snapshot.Object;
+ }
+ }
+
+ protected class TestTextChangeCollection : List, INormalizedTextChangeCollection
+ {
+ public bool IncludesLineChanges => throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs
new file mode 100644
index 0000000000..747f8d8713
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs
@@ -0,0 +1,87 @@
+// 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 Microsoft.VisualStudio.Test;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Operations;
+using Moq;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Editor.Razor
+{
+ public class BraceSmartIndenterTestBase : ForegroundDispatcherTestBase
+ {
+ protected static VisualStudioDocumentTracker CreateDocumentTracker(Func bufferAccessor, ITextView focusedTextView)
+ {
+ var tracker = new Mock();
+ tracker.Setup(t => t.TextBuffer)
+ .Returns(bufferAccessor);
+ tracker.Setup(t => t.GetFocusedTextView())
+ .Returns(focusedTextView);
+
+ return tracker.Object;
+ }
+
+ protected static VisualStudioDocumentTrackerFactory CreateDocumentTrackerFactory(Func bufferAccessor, VisualStudioDocumentTracker documentTracker)
+ {
+ var trackerFactory = new Mock();
+ trackerFactory.Setup(factory => factory.GetTracker(It.IsAny()))
+ .Returns(documentTracker);
+
+ return trackerFactory.Object;
+ }
+
+ protected static ITextView CreateFocusedTextView(Func textBufferAccessor = null, ITextCaret caret = null)
+ {
+ var focusedTextView = new Mock();
+ focusedTextView.Setup(textView => textView.HasAggregateFocus)
+ .Returns(true);
+
+ if (textBufferAccessor != null)
+ {
+ focusedTextView.Setup(textView => textView.TextBuffer)
+ .Returns(textBufferAccessor);
+ }
+
+ if (caret != null)
+ {
+ focusedTextView.Setup(textView => textView.Caret)
+ .Returns(caret);
+ }
+
+ return focusedTextView.Object;
+ }
+
+ protected static ITextCaret CreateCaretFrom(int position, ITextSnapshot snapshot)
+ {
+ var bufferPosition = new VirtualSnapshotPoint(snapshot, position);
+ var caret = new Mock();
+ caret.Setup(c => c.Position)
+ .Returns(new CaretPosition(bufferPosition, new Mock().Object, PositionAffinity.Predecessor));
+ caret.Setup(c => c.MoveTo(It.IsAny()));
+
+ return caret.Object;
+ }
+
+ protected static IEditorOperationsFactoryService CreateOperationsFactoryService()
+ {
+ var editorOperations = new Mock();
+ editorOperations.Setup(operations => operations.MoveToEndOfLine(false));
+ var editorOperationsFactory = new Mock();
+ editorOperationsFactory.Setup(factory => factory.GetEditorOperations(It.IsAny()))
+ .Returns(editorOperations.Object);
+
+ return editorOperationsFactory.Object;
+ }
+
+ protected static TestTextBuffer CreateTextBuffer(ITextSnapshot initialSnapshot, VisualStudioDocumentTracker documentTracker)
+ {
+ var textBuffer = new TestTextBuffer(initialSnapshot);
+ textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);
+
+ return textBuffer;
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestEdit.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestEdit.cs
new file mode 100644
index 0000000000..33dd99b7eb
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestEdit.cs
@@ -0,0 +1,31 @@
+// 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 Microsoft.AspNetCore.Razor.Language;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.Test
+{
+ public class TestEdit
+ {
+ public TestEdit(SourceChange change, ITextSnapshot oldSnapshot, ITextSnapshot newSnapshot)
+ {
+ Change = change;
+ OldSnapshot = oldSnapshot;
+ NewSnapshot = newSnapshot;
+ }
+
+ public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText)
+ {
+ Change = new SourceChange(position, oldLength, newText);
+ OldSnapshot = oldSnapshot;
+ NewSnapshot = newSnapshot;
+ }
+
+ public SourceChange Change { get; }
+
+ public ITextSnapshot OldSnapshot { get; }
+
+ public ITextSnapshot NewSnapshot { get; }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs
new file mode 100644
index 0000000000..5f7a658b8c
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs
@@ -0,0 +1,149 @@
+// 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.Collections.Generic;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Test
+{
+ public class TestTextBuffer : ITextBuffer
+ {
+ private ITextSnapshot _currentSnapshot;
+
+ public TestTextBuffer(ITextSnapshot initialSnapshot)
+ {
+ _currentSnapshot = initialSnapshot;
+ ReadOnlyRegionsChanged += (sender, args) => { };
+ ChangedLowPriority += (sender, args) => { };
+ ChangedHighPriority += (sender, args) => { };
+ Changing += (sender, args) => { };
+ PostChanged += (sender, args) => { };
+ ContentTypeChanged += (sender, args) => { };
+ Properties = new PropertyCollection();
+ }
+
+ public void ApplyEdit(TestEdit edit)
+ {
+ ApplyEdits(edit);
+ }
+
+ public void ApplyEdits(params TestEdit[] edits)
+ {
+ var args = new TextContentChangedEventArgs(edits[0].OldSnapshot, edits[edits.Length - 1].NewSnapshot, new EditOptions(), null);
+ foreach (var edit in edits)
+ {
+ args.Changes.Add(new TestTextChange(edit.Change));
+ }
+
+ _currentSnapshot = edits[edits.Length - 1].NewSnapshot;
+
+ Changed?.Invoke(this, args);
+ PostChanged?.Invoke(null, null);
+
+ ReadOnlyRegionsChanged?.Invoke(null, null);
+ ChangedLowPriority?.Invoke(null, null);
+ ChangedHighPriority?.Invoke(null, null);
+ Changing?.Invoke(null, null);
+ ContentTypeChanged?.Invoke(null, null);
+ }
+
+ public ITextSnapshot CurrentSnapshot => _currentSnapshot;
+
+ public PropertyCollection Properties { get; }
+
+ public event EventHandler ReadOnlyRegionsChanged;
+ public event EventHandler Changed;
+ public event EventHandler ChangedLowPriority;
+ public event EventHandler ChangedHighPriority;
+ public event EventHandler Changing;
+ public event EventHandler PostChanged;
+ public event EventHandler ContentTypeChanged;
+
+ public bool EditInProgress => throw new NotImplementedException();
+
+ public IContentType ContentType => throw new NotImplementedException();
+
+ public ITextEdit CreateEdit() => new BufferEdit(this);
+
+ public void ChangeContentType(IContentType newContentType, object editTag) => throw new NotImplementedException();
+
+ public bool CheckEditAccess() => throw new NotImplementedException();
+
+ public ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) => throw new NotImplementedException();
+
+ public IReadOnlyRegionEdit CreateReadOnlyRegionEdit() => throw new NotImplementedException();
+
+ public ITextSnapshot Delete(Span deleteSpan) => throw new NotImplementedException();
+
+ public NormalizedSpanCollection GetReadOnlyExtents(Span span) => throw new NotImplementedException();
+
+ public ITextSnapshot Insert(int position, string text) => throw new NotImplementedException();
+
+ public bool IsReadOnly(int position) => throw new NotImplementedException();
+
+ public bool IsReadOnly(int position, bool isEdit) => throw new NotImplementedException();
+
+ public bool IsReadOnly(Span span) => throw new NotImplementedException();
+
+ public bool IsReadOnly(Span span, bool isEdit) => throw new NotImplementedException();
+
+ public ITextSnapshot Replace(Text.Span replaceSpan, string replaceWith) => throw new NotImplementedException();
+
+ public void TakeThreadOwnership() => throw new NotImplementedException();
+
+ private class BufferEdit : ITextEdit
+ {
+ private readonly TestTextBuffer _textBuffer;
+ private readonly List _edits;
+
+ public BufferEdit(TestTextBuffer textBuffer)
+ {
+ _textBuffer = textBuffer;
+ _edits = new List();
+ }
+
+ public bool HasEffectiveChanges => throw new NotImplementedException();
+
+ public bool HasFailedChanges => throw new NotImplementedException();
+
+ public ITextSnapshot Snapshot => throw new NotImplementedException();
+
+ public bool Canceled => throw new NotImplementedException();
+
+ public ITextSnapshot Apply()
+ {
+ _textBuffer.ApplyEdits(_edits.ToArray());
+ _edits.Clear();
+
+ return _textBuffer.CurrentSnapshot;
+ }
+
+ public bool Insert(int position, string text)
+ {
+ var initialSnapshot = (StringTextSnapshot)_textBuffer.CurrentSnapshot;
+ var newText = initialSnapshot.Content.Insert(position, text);
+ var changedSnapshot = new StringTextSnapshot(newText);
+ var edit = new TestEdit(position, 0, initialSnapshot, text.Length, changedSnapshot, text);
+ _edits.Add(edit);
+
+ return true;
+ }
+
+ public void Cancel() => throw new NotImplementedException();
+
+ public bool Delete(Span deleteSpan) => throw new NotImplementedException();
+
+ public bool Delete(int startPosition, int charsToDelete) => throw new NotImplementedException();
+
+ public void Dispose() => throw new NotImplementedException();
+
+ public bool Insert(int position, char[] characterBuffer, int startIndex, int length) => throw new NotImplementedException();
+
+ public bool Replace(Span replaceSpan, string replaceWith) => throw new NotImplementedException();
+
+ public bool Replace(int startPosition, int charsToReplace, string replaceWith) => throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextChange.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextChange.cs
new file mode 100644
index 0000000000..7b1167c2c5
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextChange.cs
@@ -0,0 +1,50 @@
+// 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 Microsoft.AspNetCore.Razor.Language;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.Test
+{
+ public class TestTextChange : ITextChange
+ {
+ public TestTextChange(TestEdit edit) : this(edit.Change)
+ {
+ }
+
+ public TestTextChange(SourceChange change)
+ {
+ var changeSpan = change.Span;
+
+ OldPosition = changeSpan.AbsoluteIndex;
+ NewPosition = OldPosition;
+ OldEnd = changeSpan.AbsoluteIndex + changeSpan.Length;
+ NewEnd = changeSpan.AbsoluteIndex + change.NewText.Length;
+ }
+
+ public int OldPosition { get; }
+
+ public int NewPosition { get; }
+
+ public int OldEnd { get; }
+
+ public int NewEnd { get; }
+
+ public Span OldSpan => throw new NotImplementedException();
+
+ public Span NewSpan => throw new NotImplementedException();
+
+ public int Delta => throw new NotImplementedException();
+
+ public string OldText => throw new NotImplementedException();
+
+ public string NewText => throw new NotImplementedException();
+
+ public int OldLength => throw new NotImplementedException();
+
+ public int NewLength => throw new NotImplementedException();
+
+ public int LineCountDelta => throw new NotImplementedException();
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs
index a99603fe9b..544f74a91d 100644
--- a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Xunit;
@@ -574,12 +575,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var sourceChange = new SourceChange(insertionLocation, 0, insertionText);
var oldSnapshot = new StringTextSnapshot(initialText);
var changedSnapshot = new StringTextSnapshot(changedText);
- return new TestEdit
- {
- Change = sourceChange,
- OldSnapshot = oldSnapshot,
- NewSnapshot = changedSnapshot,
- };
+ return new TestEdit(sourceChange, oldSnapshot, changedSnapshot);
}
private static RazorTemplateEngine CreateTemplateEngine(
@@ -607,25 +603,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml");
return templateEngine;
}
-
- private class TestEdit
- {
- public TestEdit()
- {
- }
-
- public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText)
- {
- Change = new SourceChange(position, oldLength, newText);
- OldSnapshot = oldSnapshot;
- NewSnapshot = newSnapshot;
- }
-
- public SourceChange Change { get; set; }
-
- public ITextSnapshot OldSnapshot { get; set; }
-
- public ITextSnapshot NewSnapshot { get; set; }
- }
}
}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/TextContentChangedEventArgsExtensionsTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/TextContentChangedEventArgsExtensionsTest.cs
new file mode 100644
index 0000000000..4882a49a13
--- /dev/null
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/TextContentChangedEventArgsExtensionsTest.cs
@@ -0,0 +1,96 @@
+// 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 Microsoft.AspNetCore.Razor.Language;
+using Microsoft.VisualStudio.Test;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Text
+{
+ public class TextContentChangedEventArgsExtensionsTest
+ {
+ [Fact]
+ public void TextChangeOccurred_NoChanges_ReturnsFalse()
+ {
+ // Arrange
+ var before = new StringTextSnapshot(string.Empty);
+ var after = new StringTextSnapshot(string.Empty);
+ var testArgs = new TestTextContentChangedEventArgs(before, after);
+
+ // Act
+ var result = testArgs.TextChangeOccurred(out var changeInformation);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TextChangeOccurred_CancelingChanges_ReturnsFalse()
+ {
+ // Arrange
+ var before = new StringTextSnapshot("by");
+ before.Version.Changes.Add(new TestTextChange(new SourceChange(0, 2, "hi")));
+ before.Version.Changes.Add(new TestTextChange(new SourceChange(0, 2, "by")));
+ var after = new StringTextSnapshot("by");
+ var testArgs = new TestTextContentChangedEventArgs(before, after);
+
+ // Act
+ var result = testArgs.TextChangeOccurred(out var changeInformation);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TextChangeOccurred_SingleChange_ReturnsTrue()
+ {
+ // Arrange
+ var before = new StringTextSnapshot("by");
+ var firstChange = new TestTextChange(new SourceChange(0, 2, "hi"));
+ before.Version.Changes.Add(firstChange);
+ var after = new StringTextSnapshot("hi");
+ var testArgs = new TestTextContentChangedEventArgs(before, after);
+
+ // Act
+ var result = testArgs.TextChangeOccurred(out var changeInformation);
+
+ // Assert
+ Assert.True(result);
+ Assert.Same(firstChange, changeInformation.firstChange);
+ Assert.Equal(firstChange, changeInformation.lastChange);
+ Assert.Equal("hi", changeInformation.newText);
+ Assert.Equal("by", changeInformation.oldText);
+ }
+
+ [Fact]
+ public void TextChangeOccurred_MultipleChanges_ReturnsTrue()
+ {
+ // Arrange
+ var before = new StringTextSnapshot("by by");
+ var firstChange = new TestTextChange(new SourceChange(0, 2, "hi"));
+ before.Version.Changes.Add(firstChange);
+ var lastChange = new TestTextChange(new SourceChange(3, 2, "hi"));
+ before.Version.Changes.Add(lastChange);
+ var after = new StringTextSnapshot("hi hi");
+ var testArgs = new TestTextContentChangedEventArgs(before, after);
+
+ // Act
+ var result = testArgs.TextChangeOccurred(out var changeInformation);
+
+ // Assert
+ Assert.True(result);
+ Assert.Same(firstChange, changeInformation.firstChange);
+ Assert.Equal(lastChange, changeInformation.lastChange);
+ Assert.Equal("hi hi", changeInformation.newText);
+ Assert.Equal("by by", changeInformation.oldText);
+ }
+
+ private class TestTextContentChangedEventArgs : TextContentChangedEventArgs
+ {
+ public TestTextContentChangedEventArgs(ITextSnapshot before, ITextSnapshot after)
+ : base(before, after, EditOptions.DefaultMinimalChange, null)
+ {
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs
index c56595f421..10b3a29000 100644
--- a/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs
+++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs
@@ -9,10 +9,13 @@ using System.Threading;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
+using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
-using Microsoft.VisualStudio.Utilities;
+using Microsoft.VisualStudio.Text.Operations;
+using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
@@ -24,13 +27,31 @@ namespace Microsoft.VisualStudio.Editor.Razor
[Fact]
public void ConstructorRequiresNonNullPhysicalPath()
{
- Assert.Throws("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), null, new TestCompletionBroker()));
+ Assert.Throws("filePath",
+ () => new VisualStudioRazorParser(
+ Dispatcher,
+ new TestTextBuffer(null),
+ CreateTemplateEngine(),
+ null,
+ new DefaultErrorReporter(),
+ new TestCompletionBroker(),
+ new Mock().Object,
+ new Mock().Object));
}
[Fact]
public void ConstructorRequiresNonEmptyPhysicalPath()
{
- Assert.Throws("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker()));
+ Assert.Throws("filePath",
+ () => new VisualStudioRazorParser(
+ Dispatcher,
+ new TestTextBuffer(null),
+ CreateTemplateEngine(),
+ string.Empty,
+ new DefaultErrorReporter(),
+ new TestCompletionBroker(),
+ new Mock().Object,
+ new Mock().Object));
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
@@ -39,9 +60,17 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Arrange
var original = new StringTextSnapshot("Foo @bar Baz");
var testBuffer = new TestTextBuffer(original);
- using (var parser = new VisualStudioRazorParser(Dispatcher, testBuffer, CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker()))
+ using (var parser = new VisualStudioRazorParser(
+ Dispatcher,
+ testBuffer,
+ CreateTemplateEngine(),
+ TestLinePragmaFileName,
+ new DefaultErrorReporter(),
+ new TestCompletionBroker(),
+ new Mock().Object,
+ new Mock().Object))
{
- parser._idleTimer.Interval = 100;
+ parser.IdleDelay = TimeSpan.FromMilliseconds(100);
var changed = new StringTextSnapshot("Foo @bap Daz");
var edit = new TestEdit(7, 3, original, 3, changed, "p D");
var parseComplete = new ManualResetEventSlim();
@@ -520,7 +549,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
private TestParserManager CreateParserManager(ITextSnapshot originalSnapshot, int idleDelay = 50)
{
- var parser = new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(originalSnapshot), CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker());
+ var parser = new VisualStudioRazorParser(
+ Dispatcher,
+ new TestTextBuffer(originalSnapshot),
+ CreateTemplateEngine(),
+ TestLinePragmaFileName,
+ new DefaultErrorReporter(),
+ new TestCompletionBroker(),
+ new Mock().Object,
+ new Mock().Object);
return new TestParserManager(parser);
}
@@ -559,12 +596,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var changed = new StringTextSnapshot(after);
var old = new StringTextSnapshot(before);
var change = new SourceChange(keyword.Length, 0, keyword[keyword.Length - 1].ToString());
- var edit = new TestEdit
- {
- Change = change,
- NewSnapshot = changed,
- OldSnapshot = old
- };
+ var edit = new TestEdit(change, old, changed);
using (var manager = CreateParserManager(old))
{
manager.InitializeWithDocument(edit.OldSnapshot);
@@ -610,7 +642,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
ParseCount = 0;
// Change idle delay to be huge in order to enable us to take control of when idle methods fire.
- parser._idleTimer.Interval = TimeSpan.FromMinutes(2).TotalMilliseconds;
+ parser.IdleDelay = TimeSpan.FromMinutes(2);
_parser = parser;
parser.DocumentStructureChanged += (sender, args) =>
{
@@ -633,12 +665,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
var old = new StringTextSnapshot(string.Empty);
var initialChange = new SourceChange(0, 0, snapshot.GetText());
- var edit = new TestEdit
- {
- Change = initialChange,
- OldSnapshot = old,
- NewSnapshot = snapshot
- };
+ var edit = new TestEdit(initialChange, old, snapshot);
ApplyEditAndWaitForParse(edit);
}
@@ -667,15 +694,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void WaitForReparse()
{
- Assert.True(_parser._idleTimer.Enabled, "Expected the parser to be waiting for an idle invocation but it was not.");
+ Assert.True(_parser._idleTimer != null, "Expected the parser to be waiting for an idle invocation but it was not.");
- _parser._idleTimer.Stop();
- _parser._idleTimer.Interval = 50;
- _parser._idleTimer.Start();
+ _parser.StopIdleTimer();
+ _parser.IdleDelay = TimeSpan.FromMilliseconds(50);
+ _parser.StartIdleTimer();
DoWithTimeoutIfNotDebugging(_reparseComplete.Wait);
_reparseComplete.Reset();
- Assert.False(_parser._idleTimer.Enabled);
- _parser._idleTimer.Interval = TimeSpan.FromMinutes(2).TotalMilliseconds;
+ Assert.Null(_parser._idleTimer);
+ _parser.IdleDelay = TimeSpan.FromMinutes(2);
}
public void Dispose()
@@ -684,47 +711,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
- private class TextChange : ITextChange
- {
- public TextChange(TestEdit edit) : this(edit.Change)
- {
- }
-
- public TextChange(SourceChange change)
- {
- var changeSpan = change.Span;
-
- OldPosition = changeSpan.AbsoluteIndex;
- NewPosition = OldPosition;
- OldEnd = changeSpan.AbsoluteIndex + changeSpan.Length;
- NewEnd = changeSpan.AbsoluteIndex + change.NewText.Length;
- }
-
- public Text.Span OldSpan => throw new NotImplementedException();
-
- public Text.Span NewSpan => throw new NotImplementedException();
-
- public int OldPosition { get; }
-
- public int NewPosition { get; }
-
- public int Delta => throw new NotImplementedException();
-
- public int OldEnd { get; }
-
- public int NewEnd { get; }
-
- public string OldText => throw new NotImplementedException();
-
- public string NewText => throw new NotImplementedException();
-
- public int OldLength => throw new NotImplementedException();
-
- public int NewLength => throw new NotImplementedException();
-
- public int LineCountDelta => throw new NotImplementedException();
- }
-
private class TestCompletionBroker : ICompletionBroker
{
public ICompletionSession CreateCompletionSession(ITextView textView, ITrackingPoint triggerPoint, bool trackCaret)
@@ -757,143 +743,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new NotImplementedException();
}
}
-
- private class TestTextBuffer : Text.ITextBuffer
- {
- private ITextSnapshot _currentSnapshot;
-
- public TestTextBuffer(ITextSnapshot initialSnapshot)
- {
- _currentSnapshot = initialSnapshot;
- ReadOnlyRegionsChanged += (sender, args) => { };
- ChangedLowPriority += (sender, args) => { };
- ChangedHighPriority += (sender, args) => { };
- Changing += (sender, args) => { };
- PostChanged += (sender, args) => { };
- ContentTypeChanged += (sender, args) => { };
- }
-
- public void ApplyEdit(TestEdit edit)
- {
- var args = new TextContentChangedEventArgs(edit.OldSnapshot, edit.NewSnapshot, new EditOptions(), null);
- args.Changes.Add(new TextChange(edit));
- Changed?.Invoke(this, args);
-
- ReadOnlyRegionsChanged?.Invoke(null, null);
- ChangedLowPriority?.Invoke(null, null);
- ChangedHighPriority?.Invoke(null, null);
- Changing?.Invoke(null, null);
- PostChanged?.Invoke(null, null);
- ContentTypeChanged?.Invoke(null, null);
-
- _currentSnapshot = edit.NewSnapshot;
- }
-
- public IContentType ContentType => throw new NotImplementedException();
-
- public ITextSnapshot CurrentSnapshot => _currentSnapshot;
-
- public bool EditInProgress => throw new NotImplementedException();
-
- public PropertyCollection Properties => throw new NotImplementedException();
-
- public event EventHandler ReadOnlyRegionsChanged;
- public event EventHandler Changed;
- public event EventHandler ChangedLowPriority;
- public event EventHandler ChangedHighPriority;
- public event EventHandler Changing;
- public event EventHandler PostChanged;
- public event EventHandler ContentTypeChanged;
-
- public void ChangeContentType(IContentType newContentType, object editTag)
- {
- throw new NotImplementedException();
- }
-
- public bool CheckEditAccess()
- {
- throw new NotImplementedException();
- }
-
- public ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
- {
- throw new NotImplementedException();
- }
-
- public ITextEdit CreateEdit()
- {
- throw new NotImplementedException();
- }
-
- public IReadOnlyRegionEdit CreateReadOnlyRegionEdit()
- {
- throw new NotImplementedException();
- }
-
- public ITextSnapshot Delete(Text.Span deleteSpan)
- {
- throw new NotImplementedException();
- }
-
- public NormalizedSpanCollection GetReadOnlyExtents(Text.Span span)
- {
- throw new NotImplementedException();
- }
-
- public ITextSnapshot Insert(int position, string text)
- {
- throw new NotImplementedException();
- }
-
- public bool IsReadOnly(int position)
- {
- throw new NotImplementedException();
- }
-
- public bool IsReadOnly(int position, bool isEdit)
- {
- throw new NotImplementedException();
- }
-
- public bool IsReadOnly(Text.Span span)
- {
- throw new NotImplementedException();
- }
-
- public bool IsReadOnly(Text.Span span, bool isEdit)
- {
- throw new NotImplementedException();
- }
-
- public ITextSnapshot Replace(Text.Span replaceSpan, string replaceWith)
- {
- throw new NotImplementedException();
- }
-
- public void TakeThreadOwnership()
- {
- throw new NotImplementedException();
- }
- }
-
- private class TestEdit
- {
- public TestEdit()
- {
- }
-
- public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText)
- {
- Change = new SourceChange(position, oldLength, newText);
- OldSnapshot = oldSnapshot;
- NewSnapshot = newSnapshot;
- }
-
- public SourceChange Change { get; set; }
-
- public ITextSnapshot OldSnapshot { get; set; }
-
- public ITextSnapshot NewSnapshot { get; set; }
- }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs
index 6544ba0f85..5d9759c410 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs
@@ -217,7 +217,39 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
}
[ForegroundFact]
- public void GetTracker_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker()
+ public void GetTracker_ITextBuffer_ForRazorTextBufferWithTracker_ReturnsTracker()
+ {
+ // Arrange
+ var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
+ var textBuffer = Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
+
+ // Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
+ var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, textBuffer);
+ textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
+
+ // Act
+ var result = factory.GetTracker(textBuffer);
+
+ // Assert
+ Assert.Same(tracker, result);
+ }
+
+ [ForegroundFact]
+ public void GetTracker_ITextBuffer_NonRazorBuffer_ReturnsNull()
+ {
+ // Arrange
+ var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
+ var textBuffer = Mock.Of(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection());
+
+ // Act
+ var result = factory.GetTracker(textBuffer);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [ForegroundFact]
+ public void GetTracker_ITextView_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
@@ -244,7 +276,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
}
[ForegroundFact]
- public void GetTracker_WithoutRazorBuffer_ReturnsNull()
+ public void GetTracker_ITextView_WithoutRazorBuffer_ReturnsNull()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);