Create a VisualStudio parser to handle the editing loop.
- Renamed `RazorEditorParser` => `VisualStudioRazorParser` (maintained a copy of the original in Legacy) - Tried to make as little changes as possible to the `BackgroundParser` due to its complexity; mostly just removed the TreeStructureChanged logic since this is re-done on the Razor editor side of things. - Split the `RazorEditorParserTest`s into two separate tests. Partial parser tests and the VS parsing tests. - Updated `StringTextSnapshot` to support changes in order to test the VS parser. #1536
This commit is contained in:
parent
6e42c8d0e4
commit
24154ec4c2
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel.Composition;
|
using System.ComponentModel.Composition;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
@ -165,5 +166,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
|
|
||||||
return project.IsCapabilityMatch("DotNetCoreWeb");
|
return project.IsCapabilityMatch("DotNetCoreWeb");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<ITextView> GetTextViews(ITextBuffer textBuffer)
|
||||||
|
{
|
||||||
|
// TODO: Extract text views from buffer
|
||||||
|
|
||||||
|
return new[] { (ITextView)null };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Razor.Language;
|
||||||
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||||
using Microsoft.VisualStudio.Text;
|
using Microsoft.VisualStudio.Text;
|
||||||
|
|
||||||
namespace Microsoft.VisualStudio.LanguageServices.Razor
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
{
|
{
|
||||||
internal class BackgroundParser : IDisposable
|
internal class BackgroundParser : IDisposable
|
||||||
{
|
{
|
||||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired on the main thread.
|
/// Fired on the main thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
|
public event EventHandler<DocumentStructureChangedEventArgs> ResultsReady;
|
||||||
|
|
||||||
public bool IsIdle
|
public bool IsIdle
|
||||||
{
|
{
|
||||||
|
|
@ -62,46 +62,14 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
return _main.Lock();
|
return _main.Lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
|
protected virtual void OnResultsReady(DocumentStructureChangedEventArgs args)
|
||||||
{
|
{
|
||||||
var handler = ResultsReady;
|
using (SynchronizeMainThreadState())
|
||||||
if (handler != null)
|
|
||||||
{
|
{
|
||||||
handler(this, args);
|
ResultsReady?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<Edit> edits, CancellationToken cancelToken)
|
|
||||||
{
|
|
||||||
return TreesAreDifferent(leftTree.Root, rightTree.Root, edits.Select(edit => edit.Change), cancelToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<SourceChange> changes, CancellationToken cancelToken)
|
|
||||||
{
|
|
||||||
// Apply all the pending changes to the original tree
|
|
||||||
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
|
|
||||||
// i.e. visit the tree and find applicable changes for each node.
|
|
||||||
foreach (var change in changes)
|
|
||||||
{
|
|
||||||
cancelToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var changeOwner = leftTree.LocateOwner(change);
|
|
||||||
|
|
||||||
// Apply the change to the tree
|
|
||||||
if (changeOwner == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
|
|
||||||
changeOwner.ReplaceWith(result.EditedSpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now compare the trees
|
|
||||||
var treesDifferent = !leftTree.EquivalentTo(rightTree);
|
|
||||||
return treesDifferent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class ThreadStateBase
|
private abstract class ThreadStateBase
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -155,7 +123,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
SetThreadId(Thread.CurrentThread.ManagedThreadId);
|
SetThreadId(Thread.CurrentThread.ManagedThreadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
|
public event EventHandler<DocumentStructureChangedEventArgs> ResultsReady;
|
||||||
|
|
||||||
public CancellationToken CancelToken
|
public CancellationToken CancelToken
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +155,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
|
|
||||||
public void QueueChange(Edit edit)
|
public void QueueChange(Edit edit)
|
||||||
{
|
{
|
||||||
EnsureOnThread();
|
// Any thread can queue a change.
|
||||||
|
|
||||||
lock (_stateLock)
|
lock (_stateLock)
|
||||||
{
|
{
|
||||||
// CurrentParcel token source is not null ==> There's a parse underway
|
// CurrentParcel token source is not null ==> There's a parse underway
|
||||||
|
|
@ -217,7 +186,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReturnParcel(DocumentParseCompleteEventArgs args)
|
public void ReturnParcel(DocumentStructureChangedEventArgs args)
|
||||||
{
|
{
|
||||||
lock (_stateLock)
|
lock (_stateLock)
|
||||||
{
|
{
|
||||||
|
|
@ -307,7 +276,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
DocumentParseCompleteEventArgs args = null;
|
DocumentStructureChangedEventArgs args = null;
|
||||||
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
|
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
|
||||||
{
|
{
|
||||||
if (!linkedCancel.IsCancellationRequested)
|
if (!linkedCancel.IsCancellationRequested)
|
||||||
|
|
@ -333,14 +302,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
// Clear discarded changes list
|
// Clear discarded changes list
|
||||||
_previouslyDiscarded = null;
|
_previouslyDiscarded = null;
|
||||||
|
|
||||||
var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allEdits, parcel.CancelToken);
|
|
||||||
_currentSyntaxTree = results.GetSyntaxTree();
|
_currentSyntaxTree = results.GetSyntaxTree();
|
||||||
|
|
||||||
// Build Arguments
|
// Build Arguments
|
||||||
args = new DocumentParseCompleteEventArgs(
|
args = new DocumentStructureChangedEventArgs(
|
||||||
finalEdit.Change,
|
finalEdit.Change,
|
||||||
finalEdit.Snapshot,
|
finalEdit.Snapshot,
|
||||||
treeStructureChanged,
|
|
||||||
results);
|
results);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -416,4 +383,4 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
public ITextSnapshot Snapshot { get; set; }
|
public ITextSnapshot Snapshot { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// 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.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
internal sealed class DocumentStructureChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public DocumentStructureChangedEventArgs(
|
||||||
|
SourceChange change,
|
||||||
|
ITextSnapshot snapshot,
|
||||||
|
RazorCodeDocument codeDocument)
|
||||||
|
{
|
||||||
|
SourceChange = change;
|
||||||
|
Snapshot = snapshot;
|
||||||
|
CodeDocument = codeDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="AspNetCore.Razor.Language.SourceChange"/> which triggered the re-parse.
|
||||||
|
/// </summary>
|
||||||
|
public SourceChange SourceChange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The text snapshot used in the re-parse.
|
||||||
|
/// </summary>
|
||||||
|
public ITextSnapshot Snapshot { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The result of the parsing and code generation.
|
||||||
|
/// </summary>
|
||||||
|
public RazorCodeDocument CodeDocument { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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.Threading;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
internal class ForegroundThreadAffinitizedObject
|
||||||
|
{
|
||||||
|
private readonly Thread _foregroundThread;
|
||||||
|
|
||||||
|
public ForegroundThreadAffinitizedObject()
|
||||||
|
{
|
||||||
|
_foregroundThread = Thread.CurrentThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AssertIsForeground()
|
||||||
|
{
|
||||||
|
if (Thread.CurrentThread != _foregroundThread)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Expected to be on the foreground thread and was not.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AssertIsBackground()
|
||||||
|
{
|
||||||
|
if (Thread.CurrentThread == _foregroundThread)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Expected to be on a background thread and was not.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
// 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.AspNetCore.Razor.Language.Legacy;
|
||||||
|
using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
internal class RazorSyntaxTreePartialParser
|
||||||
|
{
|
||||||
|
private readonly RazorSyntaxTree _syntaxTree;
|
||||||
|
private Span _lastChangeOwner;
|
||||||
|
private bool _lastResultProvisional;
|
||||||
|
|
||||||
|
public RazorSyntaxTreePartialParser(RazorSyntaxTree syntaxTree)
|
||||||
|
{
|
||||||
|
_syntaxTree = syntaxTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PartialParseResultInternal Parse(SourceChange change)
|
||||||
|
{
|
||||||
|
var result = GetPartialParseResult(change);
|
||||||
|
|
||||||
|
// Remember if this was provisionally accepted for next partial parse.
|
||||||
|
_lastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PartialParseResultInternal GetPartialParseResult(SourceChange change)
|
||||||
|
{
|
||||||
|
var result = PartialParseResultInternal.Rejected;
|
||||||
|
|
||||||
|
// Try the last change owner
|
||||||
|
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
|
||||||
|
{
|
||||||
|
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
||||||
|
result = editResult.Result;
|
||||||
|
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
||||||
|
{
|
||||||
|
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the span responsible for this change
|
||||||
|
_lastChangeOwner = _syntaxTree.Root.LocateOwner(change);
|
||||||
|
|
||||||
|
if (_lastResultProvisional)
|
||||||
|
{
|
||||||
|
// Last change owner couldn't accept this, so we must do a full reparse
|
||||||
|
result = PartialParseResultInternal.Rejected;
|
||||||
|
}
|
||||||
|
else if (_lastChangeOwner != null)
|
||||||
|
{
|
||||||
|
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
||||||
|
result = editResult.Result;
|
||||||
|
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
||||||
|
{
|
||||||
|
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
// 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.Timers;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||||
|
using Microsoft.VisualStudio.Language.Intellisense;
|
||||||
|
using Microsoft.VisualStudio.Text;
|
||||||
|
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
internal class VisualStudioRazorParser : IDisposable
|
||||||
|
{
|
||||||
|
// Internal for testing.
|
||||||
|
internal readonly ITextBuffer _textBuffer;
|
||||||
|
internal readonly Timer _idleTimer;
|
||||||
|
|
||||||
|
private const int IdleDelay = 3000;
|
||||||
|
private readonly ICompletionBroker _completionBroker;
|
||||||
|
private readonly BackgroundParser _parser;
|
||||||
|
private readonly ForegroundThreadAffinitizedObject _foregroundThreadAffinitizedObject;
|
||||||
|
private RazorSyntaxTreePartialParser _partialParser;
|
||||||
|
|
||||||
|
public VisualStudioRazorParser(ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker)
|
||||||
|
{
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateEngine == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(templateEngine));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completionBroker == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(completionBroker));
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateEngine = templateEngine;
|
||||||
|
FilePath = filePath;
|
||||||
|
_textBuffer = buffer;
|
||||||
|
_completionBroker = completionBroker;
|
||||||
|
_textBuffer.Changed += TextBuffer_OnChanged;
|
||||||
|
_parser = new BackgroundParser(templateEngine, filePath);
|
||||||
|
_idleTimer = new Timer(IdleDelay);
|
||||||
|
_idleTimer.Elapsed += Onidle;
|
||||||
|
_parser.ResultsReady += OnResultsReady;
|
||||||
|
_foregroundThreadAffinitizedObject = new ForegroundThreadAffinitizedObject();
|
||||||
|
|
||||||
|
_parser.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DocumentStructureChangedEventArgs> DocumentStructureChanged;
|
||||||
|
|
||||||
|
public RazorTemplateEngine TemplateEngine { get; }
|
||||||
|
|
||||||
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
public RazorCodeDocument CodeDocument { get; private set; }
|
||||||
|
|
||||||
|
public ITextSnapshot Snapshot { get; private set; }
|
||||||
|
|
||||||
|
public void Reparse()
|
||||||
|
{
|
||||||
|
// Can be called from any thread
|
||||||
|
var snapshot = _textBuffer.CurrentSnapshot;
|
||||||
|
_parser.QueueChange(null, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_foregroundThreadAffinitizedObject.AssertIsForeground();
|
||||||
|
|
||||||
|
_textBuffer.Changed -= TextBuffer_OnChanged;
|
||||||
|
_parser.Dispose();
|
||||||
|
_idleTimer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs contentChange)
|
||||||
|
{
|
||||||
|
_foregroundThreadAffinitizedObject.AssertIsForeground();
|
||||||
|
|
||||||
|
if (contentChange.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).
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Onidle(object sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
_foregroundThreadAffinitizedObject.AssertIsBackground();
|
||||||
|
|
||||||
|
var textViews = DefaultTextViewRazorDocumentTrackerService.GetTextViews(_textBuffer);
|
||||||
|
|
||||||
|
foreach (var textView in textViews)
|
||||||
|
{
|
||||||
|
if (_completionBroker.IsCompletionActive(textView))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_idleTimer.Stop();
|
||||||
|
Reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
|
||||||
|
{
|
||||||
|
_foregroundThreadAffinitizedObject.AssertIsBackground();
|
||||||
|
|
||||||
|
if (DocumentStructureChanged != null)
|
||||||
|
{
|
||||||
|
if (args.Snapshot != _textBuffer.CurrentSnapshot)
|
||||||
|
{
|
||||||
|
// A different text change is being parsed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeDocument = args.CodeDocument;
|
||||||
|
Snapshot = args.Snapshot;
|
||||||
|
_partialParser = new RazorSyntaxTreePartialParser(CodeDocument.GetSyntaxTree());
|
||||||
|
DocumentStructureChanged(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,598 @@
|
||||||
|
// 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 System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||||
|
using Microsoft.VisualStudio.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
|
{
|
||||||
|
public class RazorEditorParser : IDisposable
|
||||||
|
{
|
||||||
|
private AspNetCore.Razor.Language.Legacy.Span _lastChangeOwner;
|
||||||
|
private AspNetCore.Razor.Language.Legacy.Span _lastAutoCompleteSpan;
|
||||||
|
private BackgroundParser _parser;
|
||||||
|
|
||||||
|
public RazorEditorParser(RazorTemplateEngine templateEngine, string filePath)
|
||||||
|
{
|
||||||
|
if (templateEngine == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(templateEngine));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(filePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
AspNetCore.Razor.Language.Resources.ArgumentCannotBeNullOrEmpty,
|
||||||
|
nameof(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateEngine = templateEngine;
|
||||||
|
FilePath = filePath;
|
||||||
|
_parser = new BackgroundParser(templateEngine, filePath);
|
||||||
|
_parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args);
|
||||||
|
_parser.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a full reparse of the document completes.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
|
||||||
|
|
||||||
|
public RazorTemplateEngine TemplateEngine { get; }
|
||||||
|
|
||||||
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
// Internal for testing.
|
||||||
|
internal RazorSyntaxTree CurrentSyntaxTree { get; private set; }
|
||||||
|
|
||||||
|
// Internal for testing.
|
||||||
|
internal bool LastResultProvisional { get; private set; }
|
||||||
|
|
||||||
|
public virtual string GetAutoCompleteString()
|
||||||
|
{
|
||||||
|
if (_lastAutoCompleteSpan?.EditHandler is AutoCompleteEditHandler editHandler)
|
||||||
|
{
|
||||||
|
return editHandler.AutoCompleteString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual PartialParseResult CheckForStructureChanges(SourceChange change, ITextSnapshot snapshot)
|
||||||
|
{
|
||||||
|
if (snapshot == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = PartialParseResultInternal.Rejected;
|
||||||
|
|
||||||
|
using (_parser.SynchronizeMainThreadState())
|
||||||
|
{
|
||||||
|
// Check if we can partial-parse
|
||||||
|
if (CurrentSyntaxTree != null && _parser.IsIdle)
|
||||||
|
{
|
||||||
|
result = TryPartialParse(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, remember if this was provisionally accepted for next partial parse
|
||||||
|
LastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
|
||||||
|
VerifyFlagsAreValid(result);
|
||||||
|
|
||||||
|
return (PartialParseResult)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_parser.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PartialParseResultInternal TryPartialParse(SourceChange change)
|
||||||
|
{
|
||||||
|
var result = PartialParseResultInternal.Rejected;
|
||||||
|
|
||||||
|
// Try the last change owner
|
||||||
|
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
|
||||||
|
{
|
||||||
|
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
||||||
|
result = editResult.Result;
|
||||||
|
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
||||||
|
{
|
||||||
|
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the span responsible for this change
|
||||||
|
_lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change);
|
||||||
|
|
||||||
|
if (LastResultProvisional)
|
||||||
|
{
|
||||||
|
// Last change owner couldn't accept this, so we must do a full reparse
|
||||||
|
result = PartialParseResultInternal.Rejected;
|
||||||
|
}
|
||||||
|
else if (_lastChangeOwner != null)
|
||||||
|
{
|
||||||
|
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
||||||
|
result = editResult.Result;
|
||||||
|
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
||||||
|
{
|
||||||
|
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
||||||
|
}
|
||||||
|
if ((result & PartialParseResultInternal.AutoCompleteBlock) == PartialParseResultInternal.AutoCompleteBlock)
|
||||||
|
{
|
||||||
|
_lastAutoCompleteSpan = _lastChangeOwner;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastAutoCompleteSpan = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
|
||||||
|
{
|
||||||
|
using (_parser.SynchronizeMainThreadState())
|
||||||
|
{
|
||||||
|
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
|
||||||
|
_lastChangeOwner = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(args != null, "Event arguments cannot be null");
|
||||||
|
EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
|
||||||
|
if (handler != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler(this, args);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
private static void VerifyFlagsAreValid(PartialParseResultInternal result)
|
||||||
|
{
|
||||||
|
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
|
||||||
|
((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected),
|
||||||
|
"Partial Parse result does not have either of Accepted or Rejected flags set");
|
||||||
|
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
|
||||||
|
((result & PartialParseResultInternal.SpanContextChanged) != PartialParseResultInternal.SpanContextChanged),
|
||||||
|
"Partial Parse result was Accepted AND had SpanContextChanged flag set");
|
||||||
|
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
|
||||||
|
((result & PartialParseResultInternal.AutoCompleteBlock) != PartialParseResultInternal.AutoCompleteBlock),
|
||||||
|
"Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
|
||||||
|
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
|
||||||
|
((result & PartialParseResultInternal.Provisional) != PartialParseResultInternal.Provisional),
|
||||||
|
"Partial Parse result was Rejected AND had Provisional flag set");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class BackgroundParser : IDisposable
|
||||||
|
{
|
||||||
|
private MainThreadState _main;
|
||||||
|
private BackgroundThread _bg;
|
||||||
|
|
||||||
|
public BackgroundParser(RazorTemplateEngine templateEngine, string filePath)
|
||||||
|
{
|
||||||
|
_main = new MainThreadState(filePath);
|
||||||
|
_bg = new BackgroundThread(_main, templateEngine, filePath);
|
||||||
|
|
||||||
|
_main.ResultsReady += (sender, args) => OnResultsReady(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired on the main thread.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
|
||||||
|
|
||||||
|
public bool IsIdle
|
||||||
|
{
|
||||||
|
get { return _main.IsIdle; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_bg.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
_main.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueueChange(SourceChange change, ITextSnapshot snapshot)
|
||||||
|
{
|
||||||
|
var edit = new Edit(change, snapshot);
|
||||||
|
_main.QueueChange(edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_main.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable SynchronizeMainThreadState()
|
||||||
|
{
|
||||||
|
return _main.Lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
|
||||||
|
{
|
||||||
|
var handler = ResultsReady;
|
||||||
|
if (handler != null)
|
||||||
|
{
|
||||||
|
handler(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<Edit> edits, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
return TreesAreDifferent(leftTree.Root, rightTree.Root, edits.Select(edit => edit.Change), cancelToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<SourceChange> changes, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
// Apply all the pending changes to the original tree
|
||||||
|
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
|
||||||
|
// i.e. visit the tree and find applicable changes for each node.
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var changeOwner = leftTree.LocateOwner(change);
|
||||||
|
|
||||||
|
// Apply the change to the tree
|
||||||
|
if (changeOwner == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
|
||||||
|
changeOwner.ReplaceWith(result.EditedSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now compare the trees
|
||||||
|
var treesDifferent = !leftTree.EquivalentTo(rightTree);
|
||||||
|
return treesDifferent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class ThreadStateBase
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
private int _id = -1;
|
||||||
|
#endif
|
||||||
|
protected ThreadStateBase()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
protected void SetThreadId(int id)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
_id = id;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
protected void EnsureOnThread()
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.Assert(_id != -1, "SetThreadId was never called!");
|
||||||
|
Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
[Conditional("DEBUG")]
|
||||||
|
protected void EnsureNotOnThread()
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.Assert(_id != -1, "SetThreadId was never called!");
|
||||||
|
Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MainThreadState : ThreadStateBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource();
|
||||||
|
private readonly ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false);
|
||||||
|
private CancellationTokenSource _currentParcelCancelSource;
|
||||||
|
|
||||||
|
private string _fileName;
|
||||||
|
private readonly object _stateLock = new object();
|
||||||
|
private IList<Edit> _changes = new List<Edit>();
|
||||||
|
|
||||||
|
public MainThreadState(string fileName)
|
||||||
|
{
|
||||||
|
_fileName = fileName;
|
||||||
|
|
||||||
|
SetThreadId(Thread.CurrentThread.ManagedThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
|
||||||
|
|
||||||
|
public CancellationToken CancelToken
|
||||||
|
{
|
||||||
|
get { return _cancelSource.Token; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsIdle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
return _currentParcelCancelSource == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cancel()
|
||||||
|
{
|
||||||
|
EnsureOnThread();
|
||||||
|
_cancelSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Lock()
|
||||||
|
{
|
||||||
|
Monitor.Enter(_stateLock);
|
||||||
|
return new DisposableAction(() => Monitor.Exit(_stateLock));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueueChange(Edit edit)
|
||||||
|
{
|
||||||
|
EnsureOnThread();
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
// CurrentParcel token source is not null ==> There's a parse underway
|
||||||
|
if (_currentParcelCancelSource != null)
|
||||||
|
{
|
||||||
|
_currentParcelCancelSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
_changes.Add(edit);
|
||||||
|
_hasParcel.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorkParcel GetParcel()
|
||||||
|
{
|
||||||
|
EnsureNotOnThread(); // Only the background thread can get a parcel
|
||||||
|
_hasParcel.Wait(_cancelSource.Token);
|
||||||
|
_hasParcel.Reset();
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
// Create a cancellation source for this parcel
|
||||||
|
_currentParcelCancelSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var changes = _changes;
|
||||||
|
_changes = new List<Edit>();
|
||||||
|
return new WorkParcel(changes, _currentParcelCancelSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReturnParcel(DocumentParseCompleteEventArgs args)
|
||||||
|
{
|
||||||
|
lock (_stateLock)
|
||||||
|
{
|
||||||
|
// Clear the current parcel cancellation source
|
||||||
|
if (_currentParcelCancelSource != null)
|
||||||
|
{
|
||||||
|
_currentParcelCancelSource.Dispose();
|
||||||
|
_currentParcelCancelSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are things waiting to be parsed, just don't fire the event because we're already out of date
|
||||||
|
if (_changes.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var handler = ResultsReady;
|
||||||
|
if (handler != null)
|
||||||
|
{
|
||||||
|
handler(this, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
if (_currentParcelCancelSource != null)
|
||||||
|
{
|
||||||
|
_currentParcelCancelSource.Dispose();
|
||||||
|
_currentParcelCancelSource = null;
|
||||||
|
}
|
||||||
|
_cancelSource.Dispose();
|
||||||
|
_hasParcel.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BackgroundThread : ThreadStateBase
|
||||||
|
{
|
||||||
|
private MainThreadState _main;
|
||||||
|
private Thread _backgroundThread;
|
||||||
|
private CancellationToken _shutdownToken;
|
||||||
|
private RazorTemplateEngine _templateEngine;
|
||||||
|
private string _filePath;
|
||||||
|
private RazorSyntaxTree _currentSyntaxTree;
|
||||||
|
private IList<Edit> _previouslyDiscarded = new List<Edit>();
|
||||||
|
|
||||||
|
public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName)
|
||||||
|
{
|
||||||
|
// Run on MAIN thread!
|
||||||
|
_main = main;
|
||||||
|
_shutdownToken = _main.CancelToken;
|
||||||
|
_templateEngine = templateEngine;
|
||||||
|
_filePath = fileName;
|
||||||
|
|
||||||
|
_backgroundThread = new Thread(WorkerLoop);
|
||||||
|
SetThreadId(_backgroundThread.ManagedThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **** ANY THREAD ****
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_backgroundThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// **** BACKGROUND THREAD ****
|
||||||
|
private void WorkerLoop()
|
||||||
|
{
|
||||||
|
var fileNameOnly = Path.GetFileName(_filePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureOnThread();
|
||||||
|
|
||||||
|
while (!_shutdownToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Grab the parcel of work to do
|
||||||
|
var parcel = _main.GetParcel();
|
||||||
|
if (parcel.Edits.Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DocumentParseCompleteEventArgs args = null;
|
||||||
|
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
|
||||||
|
{
|
||||||
|
if (!linkedCancel.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Collect ALL changes
|
||||||
|
List<Edit> allEdits;
|
||||||
|
|
||||||
|
if (_previouslyDiscarded != null)
|
||||||
|
{
|
||||||
|
allEdits = Enumerable.Concat(_previouslyDiscarded, parcel.Edits).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allEdits = parcel.Edits.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalEdit = allEdits.Last();
|
||||||
|
|
||||||
|
var results = ParseChange(finalEdit.Snapshot, linkedCancel.Token);
|
||||||
|
|
||||||
|
if (results != null && !linkedCancel.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// Clear discarded changes list
|
||||||
|
_previouslyDiscarded = null;
|
||||||
|
|
||||||
|
var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allEdits, parcel.CancelToken);
|
||||||
|
_currentSyntaxTree = results.GetSyntaxTree();
|
||||||
|
|
||||||
|
// Build Arguments
|
||||||
|
args = new DocumentParseCompleteEventArgs(
|
||||||
|
finalEdit.Change,
|
||||||
|
finalEdit.Snapshot,
|
||||||
|
treeStructureChanged,
|
||||||
|
results);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
|
||||||
|
_previouslyDiscarded = allEdits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args != null)
|
||||||
|
{
|
||||||
|
_main.ReturnParcel(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Do nothing. Just shut down.
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Clean up main thread resources
|
||||||
|
_main.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RazorCodeDocument ParseChange(ITextSnapshot snapshot, CancellationToken token)
|
||||||
|
{
|
||||||
|
EnsureOnThread();
|
||||||
|
|
||||||
|
var sourceDocument = new TextSnapshotSourceDocument(snapshot, _filePath);
|
||||||
|
var imports = _templateEngine.GetImports(_filePath);
|
||||||
|
|
||||||
|
var codeDocument = RazorCodeDocument.Create(sourceDocument, imports);
|
||||||
|
|
||||||
|
_templateEngine.GenerateCode(codeDocument);
|
||||||
|
return codeDocument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WorkParcel
|
||||||
|
{
|
||||||
|
public WorkParcel(IList<Edit> changes, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
Edits = changes;
|
||||||
|
CancelToken = cancelToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CancellationToken CancelToken { get; }
|
||||||
|
|
||||||
|
public IList<Edit> Edits { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Edit
|
||||||
|
{
|
||||||
|
public Edit(SourceChange change, ITextSnapshot snapshot)
|
||||||
|
{
|
||||||
|
Change = change;
|
||||||
|
Snapshot = snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceChange Change { get; }
|
||||||
|
|
||||||
|
public ITextSnapshot Snapshot { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30319" />
|
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30319" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.11.0" Version="11.0.61030" />
|
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.11.0" Version="11.0.61030" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.12.0" Version="12.0.30110" />
|
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.12.0" Version="12.0.30110" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Language.Intellisense" Version="$(VsShellVersion)" />
|
||||||
|
|
||||||
<!-- We need to use this version of Json.Net to maintain consistency with Visual Studio. -->
|
<!-- We need to use this version of Json.Net to maintain consistency with Visual Studio. -->
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
private static readonly ResourceManager _resourceManager
|
private static readonly ResourceManager _resourceManager
|
||||||
= new ResourceManager("Microsoft.VisualStudio.LanguageServices.Razor.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
= new ResourceManager("Microsoft.VisualStudio.LanguageServices.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");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.
|
/// An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
// 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 Microsoft.AspNetCore.Razor.Language;
|
|
||||||
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
|
||||||
using Microsoft.VisualStudio.Text;
|
|
||||||
|
|
||||||
namespace Microsoft.VisualStudio.LanguageServices.Razor
|
|
||||||
{
|
|
||||||
public class RazorEditorParser : IDisposable
|
|
||||||
{
|
|
||||||
private AspNetCore.Razor.Language.Legacy.Span _lastChangeOwner;
|
|
||||||
private AspNetCore.Razor.Language.Legacy.Span _lastAutoCompleteSpan;
|
|
||||||
private BackgroundParser _parser;
|
|
||||||
|
|
||||||
public RazorEditorParser(RazorTemplateEngine templateEngine, string filePath)
|
|
||||||
{
|
|
||||||
if (templateEngine == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(templateEngine));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
AspNetCore.Razor.Language.Resources.ArgumentCannotBeNullOrEmpty,
|
|
||||||
nameof(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
TemplateEngine = templateEngine;
|
|
||||||
FilePath = filePath;
|
|
||||||
_parser = new BackgroundParser(templateEngine, filePath);
|
|
||||||
_parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args);
|
|
||||||
_parser.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Event fired when a full reparse of the document completes.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
|
|
||||||
|
|
||||||
public RazorTemplateEngine TemplateEngine { get; }
|
|
||||||
|
|
||||||
public string FilePath { get; }
|
|
||||||
|
|
||||||
// Internal for testing.
|
|
||||||
internal RazorSyntaxTree CurrentSyntaxTree { get; private set; }
|
|
||||||
|
|
||||||
// Internal for testing.
|
|
||||||
internal bool LastResultProvisional { get; private set; }
|
|
||||||
|
|
||||||
public virtual string GetAutoCompleteString()
|
|
||||||
{
|
|
||||||
if (_lastAutoCompleteSpan?.EditHandler is AutoCompleteEditHandler editHandler)
|
|
||||||
{
|
|
||||||
return editHandler.AutoCompleteString;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual PartialParseResult CheckForStructureChanges(SourceChange change, ITextSnapshot snapshot)
|
|
||||||
{
|
|
||||||
if (snapshot == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(snapshot));
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = PartialParseResultInternal.Rejected;
|
|
||||||
|
|
||||||
using (_parser.SynchronizeMainThreadState())
|
|
||||||
{
|
|
||||||
// Check if we can partial-parse
|
|
||||||
if (CurrentSyntaxTree != null && _parser.IsIdle)
|
|
||||||
{
|
|
||||||
result = TryPartialParse(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, remember if this was provisionally accepted for next partial parse
|
|
||||||
LastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
|
|
||||||
VerifyFlagsAreValid(result);
|
|
||||||
|
|
||||||
return (PartialParseResult)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_parser.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PartialParseResultInternal TryPartialParse(SourceChange change)
|
|
||||||
{
|
|
||||||
var result = PartialParseResultInternal.Rejected;
|
|
||||||
|
|
||||||
// Try the last change owner
|
|
||||||
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
|
|
||||||
{
|
|
||||||
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
|
||||||
result = editResult.Result;
|
|
||||||
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
|
||||||
{
|
|
||||||
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Locate the span responsible for this change
|
|
||||||
_lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change);
|
|
||||||
|
|
||||||
if (LastResultProvisional)
|
|
||||||
{
|
|
||||||
// Last change owner couldn't accept this, so we must do a full reparse
|
|
||||||
result = PartialParseResultInternal.Rejected;
|
|
||||||
}
|
|
||||||
else if (_lastChangeOwner != null)
|
|
||||||
{
|
|
||||||
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
|
|
||||||
result = editResult.Result;
|
|
||||||
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
|
|
||||||
{
|
|
||||||
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
|
|
||||||
}
|
|
||||||
if ((result & PartialParseResultInternal.AutoCompleteBlock) == PartialParseResultInternal.AutoCompleteBlock)
|
|
||||||
{
|
|
||||||
_lastAutoCompleteSpan = _lastChangeOwner;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lastAutoCompleteSpan = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
|
|
||||||
{
|
|
||||||
using (_parser.SynchronizeMainThreadState())
|
|
||||||
{
|
|
||||||
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
|
|
||||||
_lastChangeOwner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Assert(args != null, "Event arguments cannot be null");
|
|
||||||
EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
|
|
||||||
if (handler != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
handler(this, args);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
|
||||||
private static void VerifyFlagsAreValid(PartialParseResultInternal result)
|
|
||||||
{
|
|
||||||
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
|
|
||||||
((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected),
|
|
||||||
"Partial Parse result does not have either of Accepted or Rejected flags set");
|
|
||||||
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
|
|
||||||
((result & PartialParseResultInternal.SpanContextChanged) != PartialParseResultInternal.SpanContextChanged),
|
|
||||||
"Partial Parse result was Accepted AND had SpanContextChanged flag set");
|
|
||||||
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
|
|
||||||
((result & PartialParseResultInternal.AutoCompleteBlock) != PartialParseResultInternal.AutoCompleteBlock),
|
|
||||||
"Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
|
|
||||||
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
|
|
||||||
((result & PartialParseResultInternal.Provisional) != PartialParseResultInternal.Provisional),
|
|
||||||
"Partial Parse result was Rejected AND had Provisional flag set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -117,6 +117,9 @@
|
||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
|
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
||||||
|
<value>Value cannot be null or an empty string.</value>
|
||||||
|
</data>
|
||||||
<data name="UnexpectedException" xml:space="preserve">
|
<data name="UnexpectedException" xml:space="preserve">
|
||||||
<value>An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.</value>
|
<value>An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,765 @@
|
||||||
|
// 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.AspNetCore.Mvc.Razor.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||||
|
using Microsoft.VisualStudio.Text;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
public class RazorSyntaxTreePartialParserTest
|
||||||
|
{
|
||||||
|
public static TheoryData TagHelperPartialParseRejectData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// change, (Block)expectedDocument
|
||||||
|
return new TheoryData<TestEdit, MarkupBlock>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p></p>", 2, " "),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock("p"))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p></p>", 6, " "),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock("p"))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p some-attr></p>", 12, " "),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"some-attr",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p some-attr></p>", 12, "ibute"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"some-attribute",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p some-attr></p>", 2, " before"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"before",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized),
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"some-attr",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TagHelperPartialParseRejectData))]
|
||||||
|
public void TagHelperTagBodiesRejectPartialChanges(object objectEdit, object expectedDocument)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var edit = (TestEdit)objectEdit;
|
||||||
|
var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly");
|
||||||
|
builder.SetTypeName("PTagHelper");
|
||||||
|
builder.TagMatchingRule(rule => rule.TagName = "p");
|
||||||
|
var descriptors = new[]
|
||||||
|
{
|
||||||
|
builder.Build()
|
||||||
|
};
|
||||||
|
var templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
|
||||||
|
var document = TestRazorCodeDocument.Create(
|
||||||
|
TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
|
||||||
|
new[] { templateEngine.Options.DefaultImports });
|
||||||
|
templateEngine.Engine.Process(document);
|
||||||
|
var syntaxTree = document.GetSyntaxTree();
|
||||||
|
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = parser.Parse(edit.Change);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(PartialParseResultInternal.Rejected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData TagHelperAttributeAcceptData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
|
||||||
|
// change, (Block)expectedDocument, partialParseResult
|
||||||
|
return new TheoryData<TestEdit, MarkupBlock, PartialParseResultInternal>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p str-attr='@DateTime'></p>", 22, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"str-attr",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupBlock(
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory
|
||||||
|
.Code("DateTime.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)))),
|
||||||
|
AttributeStructure.SingleQuotes)
|
||||||
|
})),
|
||||||
|
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p obj-attr='DateTime'></p>", 21, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"obj-attr",
|
||||||
|
factory.CodeMarkup("DateTime."),
|
||||||
|
AttributeStructure.SingleQuotes)
|
||||||
|
})),
|
||||||
|
PartialParseResultInternal.Accepted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p obj-attr='1 + DateTime'></p>", 25, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"obj-attr",
|
||||||
|
factory.CodeMarkup("1 + DateTime."),
|
||||||
|
AttributeStructure.SingleQuotes)
|
||||||
|
})),
|
||||||
|
PartialParseResultInternal.Accepted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p before-attr str-attr='@DateTime' after-attr></p>", 34, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"before-attr",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized),
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"str-attr",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupBlock(
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory
|
||||||
|
.Code("DateTime.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)))),
|
||||||
|
AttributeStructure.SingleQuotes),
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"after-attr",
|
||||||
|
value: null,
|
||||||
|
attributeStructure: AttributeStructure.Minimized),
|
||||||
|
})),
|
||||||
|
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CreateInsertionChange("<p str-attr='before @DateTime after'></p>", 29, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagHelperBlock(
|
||||||
|
"p",
|
||||||
|
attributes: new List<TagHelperAttributeNode>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeNode(
|
||||||
|
"str-attr",
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("before"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup(" "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory
|
||||||
|
.Code("DateTime.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace))),
|
||||||
|
factory.Markup(" after")),
|
||||||
|
AttributeStructure.SingleQuotes)
|
||||||
|
})),
|
||||||
|
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TagHelperAttributeAcceptData))]
|
||||||
|
public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(
|
||||||
|
object editObject,
|
||||||
|
object expectedDocument,
|
||||||
|
object partialParseResultObject)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var edit = (TestEdit)editObject;
|
||||||
|
var partialParseResult = (PartialParseResultInternal)partialParseResultObject;
|
||||||
|
var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "Test");
|
||||||
|
builder.SetTypeName("PTagHelper");
|
||||||
|
builder.TagMatchingRule(rule => rule.TagName = "p");
|
||||||
|
builder.BindAttribute(attribute =>
|
||||||
|
{
|
||||||
|
attribute.Name = "obj-attr";
|
||||||
|
attribute.TypeName = typeof(object).FullName;
|
||||||
|
attribute.SetPropertyName("ObjectAttribute");
|
||||||
|
});
|
||||||
|
builder.BindAttribute(attribute =>
|
||||||
|
{
|
||||||
|
attribute.Name = "str-attr";
|
||||||
|
attribute.TypeName = typeof(string).FullName;
|
||||||
|
attribute.SetPropertyName("StringAttribute");
|
||||||
|
});
|
||||||
|
var descriptors = new[] { builder.Build() };
|
||||||
|
var templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
|
||||||
|
var document = TestRazorCodeDocument.Create(
|
||||||
|
TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
|
||||||
|
new[] { templateEngine.Options.DefaultImports });
|
||||||
|
templateEngine.Engine.Process(document);
|
||||||
|
var syntaxTree = document.GetSyntaxTree();
|
||||||
|
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = parser.Parse(edit.Change);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(partialParseResult, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsInnerInsertionsInStatementBlock()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime..Now" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var old = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime.Now" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime..Now")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsInnerInsertions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime..Now baz");
|
||||||
|
var old = new StringTextSnapshot("foo @DateTime.Now baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(13, 0, old, 1, changed, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime..Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")), additionalFlags: PartialParseResultInternal.Provisional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsWholeIdentifierReplacement()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @date baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(5, 4, old, 8, changed, "DateTime"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var old = new StringTextSnapshot("foo @date baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @if baz");
|
||||||
|
var edit = new TestEdit(5, 4, old, 2, changed, "if");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
RunPartialParseRejectionTest(edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var old = new StringTextSnapshot("foo @date baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @inherits baz");
|
||||||
|
var edit = new TestEdit(5, 4, old, 8, changed, "inherits");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
RunPartialParseRejectionTest(edit, PartialParseResultInternal.SpanContextChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @dTime baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @dTime.Now baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime.Now baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @Datet baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(9, 1, old, 4, changed, "Time"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @DateTime.n baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime.Now baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var old = new StringTextSnapshot("foo @DateTime.n.ToString() baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime.Now.ToString() baz");
|
||||||
|
|
||||||
|
// Act and Assert
|
||||||
|
RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @User. baz");
|
||||||
|
var old = new StringTextSnapshot("foo @User.Name baz");
|
||||||
|
RunPartialParseTest(new TestEdit(10, 4, old, 0, changed, string.Empty),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")),
|
||||||
|
additionalFlags: PartialParseResultInternal.Provisional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @Us baz");
|
||||||
|
var old = new StringTextSnapshot("foo @User baz");
|
||||||
|
RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @User. baz");
|
||||||
|
var old = new StringTextSnapshot("foo @U baz");
|
||||||
|
RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")),
|
||||||
|
additionalFlags: PartialParseResultInternal.Provisional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @barbiz baz");
|
||||||
|
var old = new StringTextSnapshot("foo @bar baz");
|
||||||
|
RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @food" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var old = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @foo" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("food")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @foo.d" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var old = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @foo." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
RunPartialParseTest(new TestEdit(11 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo.d")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @foo." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var old = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @foo" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(@"foo.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @foo. bar");
|
||||||
|
var old = new StringTextSnapshot("foo @foo bar");
|
||||||
|
RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" bar")),
|
||||||
|
additionalFlags: PartialParseResultInternal.Provisional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @foob bar");
|
||||||
|
var old = new StringTextSnapshot("foo @foo bar");
|
||||||
|
RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foob")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" bar")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{@foo.b}");
|
||||||
|
var old = new StringTextSnapshot("@{@foo.}");
|
||||||
|
RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b"),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.EmptyCSharp()
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo.b")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.EmptyCSharp().AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{@foo.}");
|
||||||
|
var old = new StringTextSnapshot("@{@foo}");
|
||||||
|
RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, "."),
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.EmptyCSharp()
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo.")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.EmptyCSharp().AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunPartialParseRejectionTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0)
|
||||||
|
{
|
||||||
|
var templateEngine = CreateTemplateEngine();
|
||||||
|
var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
|
||||||
|
templateEngine.Engine.Process(document);
|
||||||
|
var syntaxTree = document.GetSyntaxTree();
|
||||||
|
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
|
||||||
|
|
||||||
|
var result = parser.Parse(edit.Change);
|
||||||
|
Assert.Equal(PartialParseResultInternal.Rejected | additionalFlags, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunPartialParseTest(TestEdit edit, Block expectedTree, PartialParseResultInternal additionalFlags = 0)
|
||||||
|
{
|
||||||
|
var templateEngine = CreateTemplateEngine();
|
||||||
|
var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
|
||||||
|
templateEngine.Engine.Process(document);
|
||||||
|
var syntaxTree = document.GetSyntaxTree();
|
||||||
|
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
|
||||||
|
|
||||||
|
var result = parser.Parse(edit.Change);
|
||||||
|
Assert.Equal(PartialParseResultInternal.Accepted | additionalFlags, result);
|
||||||
|
ParserTestBase.EvaluateParseTree(expectedTree, syntaxTree.Root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText)
|
||||||
|
{
|
||||||
|
var changedText = initialText.Insert(insertionLocation, insertionText);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RazorTemplateEngine CreateTemplateEngine(
|
||||||
|
string path = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml",
|
||||||
|
IEnumerable<TagHelperDescriptor> tagHelpers = null)
|
||||||
|
{
|
||||||
|
var engine = RazorEngine.CreateDesignTime(builder =>
|
||||||
|
{
|
||||||
|
RazorExtensions.Register(builder);
|
||||||
|
|
||||||
|
if (tagHelpers != null)
|
||||||
|
{
|
||||||
|
builder.AddTagHelpers(tagHelpers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend
|
||||||
|
// that it does.
|
||||||
|
var items = new List<RazorProjectItem>();
|
||||||
|
items.Add(new TestRazorProjectItem(path));
|
||||||
|
|
||||||
|
var project = new TestRazorProject(items);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(engine, project);
|
||||||
|
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,892 @@
|
||||||
|
// 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 System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||||
|
using Microsoft.VisualStudio.Language.Intellisense;
|
||||||
|
using Microsoft.VisualStudio.LanguageServices.Razor.Editor;
|
||||||
|
using Microsoft.VisualStudio.Text;
|
||||||
|
using Microsoft.VisualStudio.Text.Editor;
|
||||||
|
using Microsoft.VisualStudio.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
|
||||||
|
{
|
||||||
|
public class VisualStudioRazorParserTest
|
||||||
|
{
|
||||||
|
private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorRequiresNonNullPhysicalPath()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>("filePath", () => new VisualStudioRazorParser(new TestTextBuffer(null), CreateTemplateEngine(), null, new TestCompletionBroker()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstructorRequiresNonEmptyPhysicalPath()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>("filePath", () => new VisualStudioRazorParser(new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new StringTextSnapshot("Foo @bar Baz");
|
||||||
|
var testBuffer = new TestTextBuffer(original);
|
||||||
|
using (var parser = new VisualStudioRazorParser(testBuffer, CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker()))
|
||||||
|
{
|
||||||
|
parser._idleTimer.Interval = 100;
|
||||||
|
var changed = new StringTextSnapshot("Foo @bap Daz");
|
||||||
|
var edit = new TestEdit(7, 3, original, 3, changed, "p D");
|
||||||
|
var parseComplete = new ManualResetEventSlim();
|
||||||
|
var parseCount = 0;
|
||||||
|
parser.DocumentStructureChanged += (s, a) =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref parseCount);
|
||||||
|
parseComplete.Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act - 1
|
||||||
|
testBuffer.ApplyEdit(edit);
|
||||||
|
DoWithTimeoutIfNotDebugging(parseComplete.Wait); // Wait for the parse to finish
|
||||||
|
|
||||||
|
// Assert - 1
|
||||||
|
Assert.Equal(1, parseCount);
|
||||||
|
parseComplete.Reset();
|
||||||
|
|
||||||
|
// Act - 2
|
||||||
|
testBuffer.ApplyEdit(edit);
|
||||||
|
|
||||||
|
// Assert - 2
|
||||||
|
DoWithTimeoutIfNotDebugging(parseComplete.Wait);
|
||||||
|
Assert.Equal(2, parseCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AwaitPeriodInsertionAcceptedProvisionally()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var original = new StringTextSnapshot("foo @await Html baz");
|
||||||
|
using (var manager = CreateParserManager(original))
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @await Html. baz");
|
||||||
|
var edit = new TestEdit(15, 0, original, 1, changed, ".");
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
manager.ApplyEditAndWaitForReparse(edit);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("await Html").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.WhiteSpace | AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(". baz")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var original = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
|
||||||
|
var edit = new TestEdit(15 + Environment.NewLine.Length, 0, original, 1, changed, ".");
|
||||||
|
using (var manager = CreateParserManager(original))
|
||||||
|
{
|
||||||
|
void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
|
||||||
|
{
|
||||||
|
manager.ApplyEdit(testEdit);
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(expectedCode)
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime.." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
edit = new TestEdit(16 + Environment.NewLine.Length, 0, original, 1, changed, ".");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime..");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime.Now." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
edit = new TestEdit(16 + Environment.NewLine.Length, 0, original, 3, changed, "Now");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.Now.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateT." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
var original = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateT" + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
|
||||||
|
var edit = new TestEdit(12 + Environment.NewLine.Length, 0, original, 1, changed, ".");
|
||||||
|
using (var manager = CreateParserManager(original))
|
||||||
|
{
|
||||||
|
void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
|
||||||
|
{
|
||||||
|
manager.ApplyEdit(testEdit);
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.EmptyHtml(),
|
||||||
|
new StatementBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
|
||||||
|
factory.Code(Environment.NewLine + " ")
|
||||||
|
.AsStatement()
|
||||||
|
.AutoCompleteWith(autoCompleteString: null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(expectedCode)
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Code(Environment.NewLine).AsStatement(),
|
||||||
|
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateT.");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("@{" + Environment.NewLine
|
||||||
|
+ " @DateTime." + Environment.NewLine
|
||||||
|
+ "}");
|
||||||
|
edit = new TestEdit(12 + Environment.NewLine.Length, 0, original, 3, changed, "ime");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @DateT. baz");
|
||||||
|
var original = new StringTextSnapshot("foo @DateT baz");
|
||||||
|
var edit = new TestEdit(10, 0, original, 1, changed, ".");
|
||||||
|
using (var manager = CreateParserManager(original, idleDelay: 250))
|
||||||
|
{
|
||||||
|
void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
|
||||||
|
{
|
||||||
|
manager.ApplyEdit(testEdit);
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateT.");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @DateTime. baz");
|
||||||
|
edit = new TestEdit(10, 0, original, 3, changed, "ime");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.");
|
||||||
|
|
||||||
|
// Verify the reparse finally comes
|
||||||
|
manager.WaitForReparse();
|
||||||
|
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(". baz")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var changed = new StringTextSnapshot("foo @DateTime. baz");
|
||||||
|
var original = new StringTextSnapshot("foo @DateTime baz");
|
||||||
|
var edit = new TestEdit(13, 0, original, 1, changed, ".");
|
||||||
|
using (var manager = CreateParserManager(original, idleDelay: 250))
|
||||||
|
{
|
||||||
|
void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
|
||||||
|
{
|
||||||
|
manager.ApplyEdit(testEdit);
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @DateTime.. baz");
|
||||||
|
edit = new TestEdit(14, 0, original, 1, changed, ".");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime..");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @DateTime.Now. baz");
|
||||||
|
edit = new TestEdit(14, 0, original, 3, changed, "Now");
|
||||||
|
|
||||||
|
ApplyAndVerifyPartialChange(edit, "DateTime.Now.");
|
||||||
|
|
||||||
|
// Verify the reparse eventually happens
|
||||||
|
manager.WaitForReparse();
|
||||||
|
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(". baz")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
|
||||||
|
{
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var original = new StringTextSnapshot("foo @date baz");
|
||||||
|
var changed = new StringTextSnapshot("foo @date. baz");
|
||||||
|
var edit = new TestEdit(9, 0, original, 1, changed, ".");
|
||||||
|
using (var manager = CreateParserManager(original, idleDelay: 250))
|
||||||
|
{
|
||||||
|
void ApplyAndVerifyPartialChange(Action applyEdit, string expectedCode)
|
||||||
|
{
|
||||||
|
applyEdit();
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" baz")));
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
|
||||||
|
|
||||||
|
// @date => @date.
|
||||||
|
ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "date.");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @date baz");
|
||||||
|
edit = new TestEdit(9, 1, original, 0, changed, "");
|
||||||
|
|
||||||
|
// @date. => @date
|
||||||
|
ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "date");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @DateTime baz");
|
||||||
|
edit = new TestEdit(5, 4, original, 8, changed, "DateTime");
|
||||||
|
|
||||||
|
// @date => @DateTime
|
||||||
|
ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "DateTime");
|
||||||
|
|
||||||
|
original = changed;
|
||||||
|
changed = new StringTextSnapshot("foo @DateTime. baz");
|
||||||
|
edit = new TestEdit(13, 0, original, 1, changed, ".");
|
||||||
|
|
||||||
|
// @DateTime => @DateTime.
|
||||||
|
ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "DateTime.");
|
||||||
|
|
||||||
|
// Verify the reparse eventually happens
|
||||||
|
manager.WaitForReparse();
|
||||||
|
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(". baz")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo @bar"), 1, new StringTextSnapshot("foo @foo. @bar"), ".");
|
||||||
|
var charTyped = new TestEdit(14, 0, new StringTextSnapshot("foo @foo. @bar"), 1, new StringTextSnapshot("foo @foo. @barb"), "b");
|
||||||
|
using (var manager = CreateParserManager(dotTyped.OldSnapshot))
|
||||||
|
{
|
||||||
|
manager.InitializeWithDocument(dotTyped.OldSnapshot);
|
||||||
|
|
||||||
|
// Apply the dot change
|
||||||
|
manager.ApplyEditAndWaitForReparse(dotTyped);
|
||||||
|
|
||||||
|
// Act (apply the identifier start char change)
|
||||||
|
manager.ApplyEditAndWaitForParse(charTyped);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root,
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(". "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("barb")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.EmptyHtml()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo bar"), 1, new StringTextSnapshot("foo @foo. bar"), ".");
|
||||||
|
var charTyped = new TestEdit(9, 0, new StringTextSnapshot("foo @foo. bar"), 1, new StringTextSnapshot("foo @foo.b bar"), "b");
|
||||||
|
using (var manager = CreateParserManager(dotTyped.OldSnapshot, idleDelay: 250))
|
||||||
|
{
|
||||||
|
manager.InitializeWithDocument(dotTyped.OldSnapshot);
|
||||||
|
|
||||||
|
// Apply the dot change
|
||||||
|
manager.ApplyEdit(dotTyped);
|
||||||
|
|
||||||
|
// Act (apply the identifier start char change)
|
||||||
|
manager.ApplyEdit(charTyped);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, manager.ParseCount);
|
||||||
|
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root,
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("foo "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(),
|
||||||
|
factory.Code("foo.b")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
|
||||||
|
factory.Markup(" bar")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("if");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("do");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("try");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("for");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("foreach");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("while");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("switch");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("lock");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("using");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("section");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("inherits");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("functions");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("namespace");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
|
||||||
|
{
|
||||||
|
RunTypeKeywordTest("class");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestParserManager CreateParserManager(ITextSnapshot originalSnapshot, int idleDelay = 50)
|
||||||
|
{
|
||||||
|
var parser = new VisualStudioRazorParser(new TestTextBuffer(originalSnapshot), CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker());
|
||||||
|
|
||||||
|
// Normal idle delay is 3000 milliseconds, for testing we want it to be far shorter.
|
||||||
|
parser._idleTimer.Interval = idleDelay;
|
||||||
|
return new TestParserManager(parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RazorTemplateEngine CreateTemplateEngine(
|
||||||
|
string path = TestLinePragmaFileName,
|
||||||
|
IEnumerable<TagHelperDescriptor> tagHelpers = null)
|
||||||
|
{
|
||||||
|
var engine = RazorEngine.CreateDesignTime(builder =>
|
||||||
|
{
|
||||||
|
RazorExtensions.Register(builder);
|
||||||
|
|
||||||
|
if (tagHelpers != null)
|
||||||
|
{
|
||||||
|
builder.AddTagHelpers(tagHelpers);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend
|
||||||
|
// that it does.
|
||||||
|
var items = new List<RazorProjectItem>();
|
||||||
|
items.Add(new TestRazorProjectItem(path));
|
||||||
|
|
||||||
|
var project = new TestRazorProject(items);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(engine, project);
|
||||||
|
templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml");
|
||||||
|
return templateEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunTypeKeywordTest(string keyword)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var before = "@" + keyword.Substring(0, keyword.Length - 1);
|
||||||
|
var after = "@" + keyword;
|
||||||
|
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
|
||||||
|
};
|
||||||
|
using (var manager = CreateParserManager(old))
|
||||||
|
{
|
||||||
|
manager.InitializeWithDocument(edit.OldSnapshot);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
manager.ApplyEditAndWaitForParse(edit);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, manager.ParseCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoWithTimeoutIfNotDebugging(Func<int, bool> withTimeout)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
withTimeout(Timeout.Infinite);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
Assert.True(withTimeout((int)TimeSpan.FromSeconds(1).TotalMilliseconds), "Timeout expired!");
|
||||||
|
#if DEBUG
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestParserManager : IDisposable
|
||||||
|
{
|
||||||
|
public int ParseCount;
|
||||||
|
|
||||||
|
private readonly ManualResetEventSlim _parserComplete;
|
||||||
|
private readonly ManualResetEventSlim _reparseComplete;
|
||||||
|
private readonly TestTextBuffer _testBuffer;
|
||||||
|
private readonly VisualStudioRazorParser _parser;
|
||||||
|
|
||||||
|
public TestParserManager(VisualStudioRazorParser parser)
|
||||||
|
{
|
||||||
|
_parserComplete = new ManualResetEventSlim();
|
||||||
|
_reparseComplete = new ManualResetEventSlim();
|
||||||
|
_testBuffer = (TestTextBuffer)parser._textBuffer;
|
||||||
|
ParseCount = 0;
|
||||||
|
_parser = parser;
|
||||||
|
parser.DocumentStructureChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref ParseCount);
|
||||||
|
_parserComplete.Set();
|
||||||
|
|
||||||
|
if (args.SourceChange == null)
|
||||||
|
{
|
||||||
|
// Reparse occurred
|
||||||
|
_reparseComplete.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public RazorSyntaxTree CurrentSyntaxTree { get; private set; }
|
||||||
|
|
||||||
|
public void InitializeWithDocument(ITextSnapshot snapshot)
|
||||||
|
{
|
||||||
|
var old = new StringTextSnapshot(string.Empty);
|
||||||
|
var initialChange = new SourceChange(0, 0, snapshot.GetText());
|
||||||
|
var edit = new TestEdit
|
||||||
|
{
|
||||||
|
Change = initialChange,
|
||||||
|
OldSnapshot = old,
|
||||||
|
NewSnapshot = snapshot
|
||||||
|
};
|
||||||
|
ApplyEditAndWaitForParse(edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyEdit(TestEdit edit)
|
||||||
|
{
|
||||||
|
_testBuffer.ApplyEdit(edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyEditAndWaitForParse(TestEdit edit)
|
||||||
|
{
|
||||||
|
ApplyEdit(edit);
|
||||||
|
WaitForParse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyEditAndWaitForReparse(TestEdit edit)
|
||||||
|
{
|
||||||
|
ApplyEdit(edit);
|
||||||
|
WaitForReparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WaitForParse()
|
||||||
|
{
|
||||||
|
DoWithTimeoutIfNotDebugging(_parserComplete.Wait); // Wait for the parse to finish
|
||||||
|
_parserComplete.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WaitForReparse()
|
||||||
|
{
|
||||||
|
DoWithTimeoutIfNotDebugging(_reparseComplete.Wait);
|
||||||
|
_reparseComplete.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_parser.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DismissAllSessions(ITextView textView)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyCollection<ICompletionSession> GetSessions(ITextView textView)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompletionActive(ITextView textView)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICompletionSession TriggerCompletion(ITextView textView)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICompletionSession TriggerCompletion(ITextView textView, ITrackingPoint triggerPoint, bool trackCaret)
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -323,7 +323,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
modified.LinkNodes();
|
modified.LinkNodes();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var treesAreDifferent = BackgroundParser.TreesAreDifferent(
|
var treesAreDifferent = RazorEditorParser.BackgroundParser.TreesAreDifferent(
|
||||||
original,
|
original,
|
||||||
modified,
|
modified,
|
||||||
new[]
|
new[]
|
||||||
|
|
@ -355,7 +355,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
factory.Code("f")
|
factory.Code("f")
|
||||||
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
|
||||||
factory.Markup("</p>"));
|
factory.Markup("</p>"));
|
||||||
Assert.True(BackgroundParser.TreesAreDifferent(
|
Assert.True(RazorEditorParser.BackgroundParser.TreesAreDifferent(
|
||||||
original,
|
original,
|
||||||
modified,
|
modified,
|
||||||
new[]
|
new[]
|
||||||
|
|
@ -386,7 +386,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
|
||||||
factory.Markup("</p>"));
|
factory.Markup("</p>"));
|
||||||
original.LinkNodes();
|
original.LinkNodes();
|
||||||
modified.LinkNodes();
|
modified.LinkNodes();
|
||||||
Assert.False(BackgroundParser.TreesAreDifferent(
|
Assert.False(RazorEditorParser.BackgroundParser.TreesAreDifferent(
|
||||||
original,
|
original,
|
||||||
modified,
|
modified,
|
||||||
new[]
|
new[]
|
||||||
|
|
@ -11,96 +11,91 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
|
||||||
{
|
{
|
||||||
public class StringTextSnapshot : ITextSnapshot
|
public class StringTextSnapshot : ITextSnapshot
|
||||||
{
|
{
|
||||||
private readonly string _content;
|
|
||||||
|
|
||||||
public StringTextSnapshot(string content)
|
public StringTextSnapshot(string content)
|
||||||
{
|
{
|
||||||
_content = content;
|
Content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public char this[int position] => _content[position];
|
public string Content { get; }
|
||||||
|
|
||||||
|
public char this[int position] => Content[position];
|
||||||
|
|
||||||
|
public ITextVersion Version { get; } = new TextVersion();
|
||||||
|
|
||||||
|
public int Length => Content.Length;
|
||||||
|
|
||||||
public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
|
public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
|
||||||
|
|
||||||
public IContentType ContentType => throw new NotImplementedException();
|
public IContentType ContentType => throw new NotImplementedException();
|
||||||
|
|
||||||
public ITextVersion Version => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public int Length => _content.Length;
|
|
||||||
|
|
||||||
public int LineCount => throw new NotImplementedException();
|
public int LineCount => throw new NotImplementedException();
|
||||||
|
|
||||||
public IEnumerable<ITextSnapshotLine> Lines => throw new NotImplementedException();
|
public IEnumerable<ITextSnapshotLine> Lines => throw new NotImplementedException();
|
||||||
|
|
||||||
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
|
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) => Content.CopyTo(sourceIndex, destination, destinationIndex, count);
|
||||||
|
|
||||||
|
public string GetText(int startIndex, int length) => Content.Substring(startIndex, length);
|
||||||
|
|
||||||
|
public string GetText() => Content;
|
||||||
|
|
||||||
|
public char[] ToCharArray(int startIndex, int length) => Content.ToCharArray();
|
||||||
|
|
||||||
|
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
public void Write(TextWriter writer, VisualStudio.Text.Span span) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void Write(TextWriter writer) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
private class TextVersion : ITextVersion
|
||||||
{
|
{
|
||||||
_content.CopyTo(sourceIndex, destination, destinationIndex, count);
|
public INormalizedTextChangeCollection Changes { get; } = new TextChangeCollection();
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
|
public ITextVersion Next => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
|
public int Length => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode)
|
public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
|
public int VersionNumber => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode)
|
public int ReiteratedVersionNumber => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
|
public ITrackingSpan CreateCustomTrackingSpan(VisualStudio.Text.Span span, TrackingFidelityMode trackingFidelity, object customState, CustomTrackToVersion behavior) => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITextSnapshotLine GetLineFromLineNumber(int lineNumber)
|
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ITextSnapshotLine GetLineFromPosition(int position)
|
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetLineNumberFromPosition(int position)
|
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetText(VisualStudio.Text.Span span)
|
public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetText(int startIndex, int length) => _content.Substring(startIndex, length);
|
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) => throw new NotImplementedException();
|
||||||
|
|
||||||
public string GetText() => _content;
|
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
|
||||||
|
|
||||||
public char[] ToCharArray(int startIndex, int length) => _content.ToCharArray();
|
private class TextChangeCollection : List<ITextChange>, INormalizedTextChangeCollection
|
||||||
|
{
|
||||||
public void Write(TextWriter writer, VisualStudio.Text.Span span)
|
public bool IncludesLineChanges => false;
|
||||||
{
|
}
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Write(TextWriter writer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue