Add smart indentation for brace completion.

- Added a standalone brace smart indenter that listens to `ITextBuffer` changed events to determine when a brace completion event needs to be handled.
- Added methods to deal with getting document trackers from `ITextBuffer`s.
- Added a `BraceSmartIndenterTest` and `BraceSmartIndenterIntegrationTest` to verify all parts of the smart indenter.
- Moved private test infrastructure classes into their own files and expanded on their functionality to enable the brace completion smart indent scenarios.

#1538
This commit is contained in:
N. Taylor Mullen 2017-10-05 21:59:49 -07:00
parent f4e9ddad22
commit 31c16af40b
22 changed files with 1638 additions and 301 deletions

View File

@ -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)

View File

@ -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")]

View File

@ -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
{
/// <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 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; }
}
}
}

View File

@ -0,0 +1,44 @@
// <auto-generated />
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);
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
internal static string ArgumentCannotBeNullOrEmpty
{
get => GetString("ArgumentCannotBeNullOrEmpty");
}
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or an empty string.</value>
</data>
</root>

View File

@ -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;
}
}
}

View File

@ -25,5 +25,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract ITextBuffer TextBuffer { get; }
public abstract IReadOnlyList<ITextView> TextViews { get; }
public abstract ITextView GetFocusedTextView();
}
}

View File

@ -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);
}
}

View File

@ -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<ITextView>();
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();

View File

@ -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

View File

@ -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<ITextBuffer> subjectBuffers)
{
if (textView == null)

View File

@ -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<ITextSnapshotLine> _lines;
public StringTextSnapshot(string content)
{
Content = content;
_lines = new List<ITextSnapshotLine>();
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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ITextCaret>();
caret.Setup(c => c.MoveTo(It.IsAny<SnapshotPoint>()))
.Callback<SnapshotPoint>(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<IEditorOperations>();
editorOperations.Setup(operations => operations.MoveToEndOfLine(false));
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
editorOperationsFactory.Setup(factory => factory.GetEditorOperations(textView))
.Returns(editorOperations.Object);
var smartIndenter = new BraceSmartIndenter(Dispatcher, new Mock<ITextBuffer>().Object, new Mock<VisualStudioDocumentTrackerFactory>().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<ITextBuffer>();
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
var changeCollection = new TestTextChangeCollection();
var textContentChangeArgs = new TestTextContentChangedEventArgs(changeCollection);
var documentTrackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
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<IEditorOperationsFactoryService>();
var documentTrackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
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<ITextSnapshot>().Object, EditOptions.DefaultMinimalChange, null)
{
}
protected static ITextSnapshot CreateBeforeSnapshot(INormalizedTextChangeCollection collection)
{
var version = new Mock<ITextVersion>();
version.Setup(v => v.Changes)
.Returns(collection);
var snapshot = new Mock<ITextSnapshot>();
snapshot.Setup(obj => obj.Version)
.Returns(version.Object);
return snapshot.Object;
}
}
protected class TestTextChangeCollection : List<ITextChange>, INormalizedTextChangeCollection
{
public bool IncludesLineChanges => throw new NotImplementedException();
}
}
}

View File

@ -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<ITextBuffer> bufferAccessor, ITextView focusedTextView)
{
var tracker = new Mock<VisualStudioDocumentTracker>();
tracker.Setup(t => t.TextBuffer)
.Returns(bufferAccessor);
tracker.Setup(t => t.GetFocusedTextView())
.Returns(focusedTextView);
return tracker.Object;
}
protected static VisualStudioDocumentTrackerFactory CreateDocumentTrackerFactory(Func<ITextBuffer> bufferAccessor, VisualStudioDocumentTracker documentTracker)
{
var trackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
trackerFactory.Setup(factory => factory.GetTracker(It.IsAny<ITextBuffer>()))
.Returns(documentTracker);
return trackerFactory.Object;
}
protected static ITextView CreateFocusedTextView(Func<ITextBuffer> textBufferAccessor = null, ITextCaret caret = null)
{
var focusedTextView = new Mock<ITextView>();
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<ITextCaret>();
caret.Setup(c => c.Position)
.Returns(new CaretPosition(bufferPosition, new Mock<IMappingPoint>().Object, PositionAffinity.Predecessor));
caret.Setup(c => c.MoveTo(It.IsAny<SnapshotPoint>()));
return caret.Object;
}
protected static IEditorOperationsFactoryService CreateOperationsFactoryService()
{
var editorOperations = new Mock<IEditorOperations>();
editorOperations.Setup(operations => operations.MoveToEndOfLine(false));
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
editorOperationsFactory.Setup(factory => factory.GetEditorOperations(It.IsAny<ITextView>()))
.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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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<SnapshotSpanEventArgs> ReadOnlyRegionsChanged;
public event EventHandler<TextContentChangedEventArgs> Changed;
public event EventHandler<TextContentChangedEventArgs> ChangedLowPriority;
public event EventHandler<TextContentChangedEventArgs> ChangedHighPriority;
public event EventHandler<TextContentChangingEventArgs> Changing;
public event EventHandler PostChanged;
public event EventHandler<ContentTypeChangedEventArgs> 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<TestEdit> _edits;
public BufferEdit(TestTextBuffer textBuffer)
{
_textBuffer = textBuffer;
_edits = new List<TestEdit>();
}
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();
}
}
}

View File

@ -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();
}
}

View File

@ -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; }
}
}
}

View File

@ -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)
{
}
}
}
}

View File

@ -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<ArgumentException>("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), null, new TestCompletionBroker()));
Assert.Throws<ArgumentException>("filePath",
() => new VisualStudioRazorParser(
Dispatcher,
new TestTextBuffer(null),
CreateTemplateEngine(),
null,
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().Object));
}
[Fact]
public void ConstructorRequiresNonEmptyPhysicalPath()
{
Assert.Throws<ArgumentException>("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker()));
Assert.Throws<ArgumentException>("filePath",
() => new VisualStudioRazorParser(
Dispatcher,
new TestTextBuffer(null),
CreateTemplateEngine(),
string.Empty,
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().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<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().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<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().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<SnapshotSpanEventArgs> ReadOnlyRegionsChanged;
public event EventHandler<TextContentChangedEventArgs> Changed;
public event EventHandler<TextContentChangedEventArgs> ChangedLowPriority;
public event EventHandler<TextContentChangedEventArgs> ChangedHighPriority;
public event EventHandler<TextContentChangingEventArgs> Changing;
public event EventHandler PostChanged;
public event EventHandler<ContentTypeChangedEventArgs> 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; }
}
}
}

View File

@ -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<ITextBuffer>(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<ITextBuffer>(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);