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:
parent
f4e9ddad22
commit
31c16af40b
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,5 +25,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
|
|||
public abstract ITextBuffer TextBuffer { get; }
|
||||
|
||||
public abstract IReadOnlyList<ITextView> TextViews { get; }
|
||||
|
||||
public abstract ITextView GetFocusedTextView();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue