Manage VisualStudioRazorParser lifetime.

- Exposed `VisualStudioRazorParser`, `DocumentStructureChangedEventArgs` and `ICanHasContextChangedListener` as ways to consume the new parser for a Razor document.
- Split the `VisualStudioRazorParser` into an abstract base and an implementation to avoid internal constructors.
- Changed the parser and corresponding smart indenter to take in document trackers, template engine factories and parser context change listeners. Of these additions the parser context change listeners will be deprecated once we own the TagHelper discovery mechanisms.
- Changed how the parser manages its internal parsing life cycle. It now creates template engines when the document tracker tells it to. So when project changes happen or new documents are opened the parser will re-instantiate its internal parser to ensure that it is parsing against the correct configurations.
- Removed all accessor services in favor of a singular RazorEditorFactoryService. This service is responsible for retrieving/creating various Razor components.
- Changed the code document provider to now use the parser provider in order to locate code documents associated with buffers. Prior to this that logic was hard coded.
- Removed old template engine reconstruction logic in the document tracker now that the parser owns that piece.
- Updated tracker to notify listeners when it's unsubscribing. This is how listeners can know when to tear bits down.
- Refactored/added pieces to the `DefaultVisualStudioRazorParser` in order to improve its unit/integration testing ability.
- Updated existing tests to react to new signatures.
- Added new visual studio razor parser tests, uncommented existing ones, and re-enforced ones that were previously flakey.
- Added various tests for the new services added, i.e. text buffer factory service tests.

#1630
This commit is contained in:
N. Taylor Mullen 2017-10-13 12:16:20 -07:00
parent a0733ffa91
commit 212d97e511
31 changed files with 1945 additions and 967 deletions

View File

@ -29,15 +29,19 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
private readonly ForegroundDispatcher _dispatcher;
private readonly ITextBuffer _textBuffer;
private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory;
private readonly VisualStudioDocumentTracker _documentTracker;
private readonly IEditorOperationsFactoryService _editorOperationsFactory;
private readonly StringBuilder _indentBuilder = new StringBuilder();
private BraceIndentationContext _context;
// Internal for testing
internal BraceSmartIndenter()
{
}
public BraceSmartIndenter(
ForegroundDispatcher dispatcher,
ITextBuffer textBuffer,
VisualStudioDocumentTrackerFactory documentTrackerFactory,
VisualStudioDocumentTracker documentTracker,
IEditorOperationsFactoryService editorOperationsFactory)
{
if (dispatcher == null)
@ -45,14 +49,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(dispatcher));
}
if (textBuffer == null)
if (documentTracker == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (documentTrackerFactory == null)
{
throw new ArgumentNullException(nameof(documentTrackerFactory));
throw new ArgumentNullException(nameof(documentTracker));
}
if (editorOperationsFactory == null)
@ -61,9 +60,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
_dispatcher = dispatcher;
_textBuffer = textBuffer;
_documentTrackerFactory = documentTrackerFactory;
_documentTracker = documentTracker;
_editorOperationsFactory = editorOperationsFactory;
_textBuffer = _documentTracker.TextBuffer;
_textBuffer.Changed += TextBuffer_OnChanged;
_textBuffer.PostChanged += TextBuffer_OnPostChanged;
}
@ -95,16 +94,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
return;
}
var documentTracker = _documentTrackerFactory.GetTracker(_textBuffer);
// Extra hardening, this should never be null.
if (documentTracker == null)
{
return;
}
var newText = changeInformation.newText;
if (TryCreateIndentationContext(changeInformation.firstChange.NewPosition, newText.Length, newText, documentTracker, out var context))
if (TryCreateIndentationContext(changeInformation.firstChange.NewPosition, newText.Length, newText, _documentTracker, out var context))
{
_context = context;
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.VisualStudio.Editor.Razor
{
internal abstract class BraceSmartIndenterFactory
{
public abstract BraceSmartIndenter Create(VisualStudioDocumentTracker documentTracker);
}
}

View File

@ -12,6 +12,19 @@ namespace Microsoft.VisualStudio.Editor.Razor
[Export(typeof(TextBufferCodeDocumentProvider))]
internal class DefaultTextBufferCodeDocumentProvider : TextBufferCodeDocumentProvider
{
private readonly RazorEditorFactoryService _editorFactoryService;
[ImportingConstructor]
public DefaultTextBufferCodeDocumentProvider(RazorEditorFactoryService editorFactoryService)
{
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
_editorFactoryService = editorFactoryService;
}
public override bool TryGetFromBuffer(ITextBuffer textBuffer, out RazorCodeDocument codeDocument)
{
if (textBuffer == null)
@ -19,8 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(textBuffer));
}
// Hack until we own the lifetime of the parser.
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser) && parser.CodeDocument != null)
if (_editorFactoryService.TryGetParser(textBuffer, out var parser) && parser.CodeDocument != null)
{
codeDocument = parser.CodeDocument;
return true;

View File

@ -0,0 +1,423 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
using Timer = System.Threading.Timer;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class DefaultVisualStudioRazorParser : VisualStudioRazorParser, IDisposable
{
public override event EventHandler<DocumentStructureChangedEventArgs> DocumentStructureChanged;
// Internal for testing.
internal TimeSpan IdleDelay = TimeSpan.FromSeconds(3);
internal Timer _idleTimer;
internal BackgroundParser _parser;
private readonly object IdleLock = new object();
private readonly ICompletionBroker _completionBroker;
private readonly IEnumerable<IContextChangedListener> _contextChangedListeners;
private readonly VisualStudioDocumentTracker _documentTracker;
private readonly ForegroundDispatcher _dispatcher;
private readonly RazorTemplateEngineFactoryService _templateEngineFactory;
private readonly ErrorReporter _errorReporter;
private RazorSyntaxTreePartialParser _partialParser;
private RazorTemplateEngine _templateEngine;
private RazorCodeDocument _codeDocument;
private ITextSnapshot _snapshot;
// For testing only
internal DefaultVisualStudioRazorParser(RazorCodeDocument codeDocument)
{
_codeDocument = codeDocument;
}
public DefaultVisualStudioRazorParser(
ForegroundDispatcher dispatcher,
VisualStudioDocumentTracker documentTracker,
RazorTemplateEngineFactoryService templateEngineFactory,
ErrorReporter errorReporter,
ICompletionBroker completionBroker,
IEnumerable<IContextChangedListener> contextChangedListeners)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
if (documentTracker == null)
{
throw new ArgumentNullException(nameof(documentTracker));
}
if (templateEngineFactory == null)
{
throw new ArgumentNullException(nameof(templateEngineFactory));
}
if (errorReporter == null)
{
throw new ArgumentNullException(nameof(errorReporter));
}
if (completionBroker == null)
{
throw new ArgumentNullException(nameof(completionBroker));
}
if (contextChangedListeners == null)
{
throw new ArgumentNullException(nameof(contextChangedListeners));
}
_dispatcher = dispatcher;
_templateEngineFactory = templateEngineFactory;
_errorReporter = errorReporter;
_completionBroker = completionBroker;
_contextChangedListeners = contextChangedListeners;
_documentTracker = documentTracker;
_documentTracker.ContextChanged += DocumentTracker_ContextChanged;
}
public override RazorTemplateEngine TemplateEngine => _templateEngine;
public override string FilePath => _documentTracker.FilePath;
public override RazorCodeDocument CodeDocument => _codeDocument;
public override ITextSnapshot Snapshot => _snapshot;
public override ITextBuffer TextBuffer => _documentTracker.TextBuffer;
// Used in unit tests to ensure we can be notified when idle starts.
internal ManualResetEventSlim NotifyForegroundIdleStart { get; set; }
// Used in unit tests to ensure we can block background idle work.
internal ManualResetEventSlim BlockBackgroundIdleWork { get; set; }
public override async Task ReparseAsync()
{
// Can be called from any thread
if (_dispatcher.IsForegroundThread)
{
ReparseOnForeground(null);
}
else
{
await Task.Factory.StartNew(ReparseOnForeground, null, CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
}
}
public void Dispose()
{
_dispatcher.AssertForegroundThread();
StopParser();
_documentTracker.ContextChanged -= DocumentTracker_ContextChanged;
StopIdleTimer();
}
// Internal for testing
internal void DocumentTracker_ContextChanged(object sender, EventArgs args)
{
_dispatcher.AssertForegroundThread();
if (!TryReinitializeParser())
{
return;
}
NotifyParserContextChanged();
// We have a new parser, force a reparse to generate new document information. Note that this
// only blocks until the reparse change has been queued.
ReparseAsync().GetAwaiter().GetResult();
}
// Internal for testing
internal bool TryReinitializeParser()
{
_dispatcher.AssertForegroundThread();
StopParser();
if (!_documentTracker.IsSupportedProject)
{
// Tracker is either starting up, tearing down or wrongfully instantiated.
// Either way, the tracker can't act on its associated project, neither can we.
return false;
}
StartParser();
return true;
}
// Internal for testing
internal void NotifyParserContextChanged()
{
_dispatcher.AssertForegroundThread();
// This is temporary until we own the TagHelper resolution system. At that point the parser will push out updates
// via DocumentStructureChangedEvents when contexts change. For now, listeners need to know more information about
// the parser. In the case that the tracker does not belong to a supported project the editor will tear down its
// attachment to the parser when it recognizes the document closing.
foreach (var contextChangeListener in _contextChangedListeners)
{
contextChangeListener.OnContextChanged(this);
}
}
// Internal for testing
internal void StartParser()
{
_dispatcher.AssertForegroundThread();
var projectDirectory = Path.GetDirectoryName(_documentTracker.ProjectPath);
_templateEngine = _templateEngineFactory.Create(projectDirectory, ConfigureTemplateEngine);
_parser = new BackgroundParser(TemplateEngine, FilePath);
_parser.ResultsReady += OnResultsReady;
_parser.Start();
TextBuffer.Changed += TextBuffer_OnChanged;
}
// Internal for testing
internal void StopParser()
{
_dispatcher.AssertForegroundThread();
if (_parser != null)
{
// Detatch from the text buffer until we have a new parser to handle changes.
TextBuffer.Changed -= TextBuffer_OnChanged;
_parser.ResultsReady -= OnResultsReady;
_parser.Dispose();
_parser = null;
}
}
// Internal for testing
internal void StartIdleTimer()
{
_dispatcher.AssertForegroundThread();
lock (IdleLock)
{
if (_idleTimer == null)
{
// Timer will fire after a fixed delay, but only once.
_idleTimer = new Timer(Timer_Tick, null, IdleDelay, Timeout.InfiniteTimeSpan);
}
}
}
// Internal for testing
internal void StopIdleTimer()
{
// Can be called from any thread.
lock (IdleLock)
{
if (_idleTimer != null)
{
_idleTimer.Dispose();
_idleTimer = null;
}
}
}
private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs args)
{
_dispatcher.AssertForegroundThread();
if (args.Changes.Count > 0)
{
// Idle timers are used to track provisional changes. Provisional changes only last for a single text change. After that normal
// partial parsing rules apply (stop the timer).
StopIdleTimer();
}
if (!args.TextChangeOccurred(out var changeInformation))
{
return;
}
var change = new SourceChange(changeInformation.firstChange.OldPosition, changeInformation.oldText.Length, changeInformation.newText);
var snapshot = args.After;
var result = PartialParseResultInternal.Rejected;
using (_parser.SynchronizeMainThreadState())
{
// Check if we can partial-parse
if (_partialParser != null && _parser.IsIdle)
{
result = _partialParser.Parse(change);
}
}
// If partial parsing failed or there were outstanding parser tasks, start a full reparse
if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
{
_parser.QueueChange(change, snapshot);
}
if ((result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional)
{
StartIdleTimer();
}
}
private void OnIdle(object state)
{
_dispatcher.AssertForegroundThread();
OnNotifyForegroundIdle();
foreach (var textView in _documentTracker.TextViews)
{
if (_completionBroker.IsCompletionActive(textView))
{
// Completion list is still active, need to re-start timer.
StartIdleTimer();
return;
}
}
// This only blocks until the reparse change has been queued.
ReparseAsync().GetAwaiter().GetResult();
}
private void ReparseOnForeground(object state)
{
_dispatcher.AssertForegroundThread();
if (_parser == null)
{
Debug.Fail("Reparse being attempted after the parser has been disposed.");
return;
}
var snapshot = TextBuffer.CurrentSnapshot;
_parser.QueueChange(null, snapshot);
}
private void OnNotifyForegroundIdle()
{
if (NotifyForegroundIdleStart != null)
{
NotifyForegroundIdleStart.Set();
}
}
private void OnStartingBackgroundIdleWork()
{
if (BlockBackgroundIdleWork != null)
{
BlockBackgroundIdleWork.Wait();
}
}
private async void Timer_Tick(object state)
{
try
{
_dispatcher.AssertBackgroundThread();
OnStartingBackgroundIdleWork();
StopIdleTimer();
// We need to get back to the UI thread to properly check if a completion is active.
await Task.Factory.StartNew(OnIdle, null, CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
}
catch (Exception ex)
{
// This is something totally unexpected, let's just send it over to the workspace.
await Task.Factory.StartNew(() => _errorReporter.ReportError(ex), CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
}
}
private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
{
_dispatcher.AssertBackgroundThread();
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);
}
}
private void ConfigureTemplateEngine(IRazorEngineBuilder builder)
{
builder.Features.Add(new VisualStudioParserOptionsFeature());
builder.Features.Add(new VisualStudioTagHelperFeature(TextBuffer));
}
/// <summary>
/// This class will cease to be useful once we harvest/monitor settings from the editor.
/// </summary>
private class VisualStudioParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
{
public int Order { get; set; }
public void Configure(RazorCodeGenerationOptionsBuilder options)
{
options.IndentSize = 4;
options.IndentWithTabs = false;
}
}
/// <summary>
/// This class will cease to be useful once we control TagHelper discovery. For now, it delegates discovery
/// to ITagHelperFeature's that exist on the text buffer.
/// </summary>
private class VisualStudioTagHelperFeature : ITagHelperFeature
{
private readonly ITextBuffer _textBuffer;
public VisualStudioTagHelperFeature(ITextBuffer textBuffer)
{
_textBuffer = textBuffer;
}
public RazorEngine Engine { get; set; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
if (_textBuffer.Properties.TryGetProperty(typeof(ITagHelperFeature), out ITagHelperFeature feature))
{
return feature.GetDescriptors();
}
return Array.Empty<TagHelperDescriptor>();
}
}
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal sealed class DocumentStructureChangedEventArgs : EventArgs
public sealed class DocumentStructureChangedEventArgs : EventArgs
{
public DocumentStructureChangedEventArgs(
SourceChange change,

View File

@ -0,0 +1,13 @@
// 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.
namespace Microsoft.VisualStudio.Editor.Razor
{
/// <summary>
/// This class will cease to be useful once the Razor tooling owns TagHelper discovery
/// </summary>
public interface IContextChangedListener
{
void OnContextChanged(VisualStudioRazorParser parser);
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.Editor.Razor
{
public abstract class RazorEditorFactoryService
{
public abstract bool TryGetDocumentTracker(ITextBuffer textBuffer, out VisualStudioDocumentTracker documentTracker);
internal abstract bool TryGetParser(ITextBuffer textBuffer, out VisualStudioRazorParser parser);
internal abstract bool TryGetSmartIndenter(ITextBuffer textBuffer, out BraceSmartIndenter braceSmartIndenter);
}
}

View File

@ -18,6 +18,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract bool IsSupportedProject { get; }
public abstract string FilePath { get; }
public abstract string ProjectPath { get; }
public abstract Project Project { get; }
public abstract Workspace Workspace { get; }

View File

@ -2,14 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.Editor.Razor
{
public abstract class VisualStudioDocumentTrackerFactory
internal abstract class VisualStudioDocumentTrackerFactory
{
public abstract VisualStudioDocumentTracker GetTracker(ITextView textView);
public abstract VisualStudioDocumentTracker GetTracker(ITextBuffer textBuffer);
public abstract VisualStudioDocumentTracker Create(ITextBuffer textBuffer);
}
}

View File

@ -2,266 +2,26 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
using Timer = System.Threading.Timer;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class VisualStudioRazorParser : IDisposable
public abstract class VisualStudioRazorParser
{
// Internal for testing.
internal readonly ITextBuffer _textBuffer;
internal TimeSpan IdleDelay = TimeSpan.FromSeconds(3);
internal Timer _idleTimer;
public abstract event EventHandler<DocumentStructureChangedEventArgs> DocumentStructureChanged;
private readonly object IdleLock = new object();
private readonly ICompletionBroker _completionBroker;
private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory;
private readonly BackgroundParser _parser;
private readonly ForegroundDispatcher _dispatcher;
private readonly ErrorReporter _errorReporter;
private RazorSyntaxTreePartialParser _partialParser;
private BraceSmartIndenter _braceSmartIndenter;
public abstract RazorTemplateEngine TemplateEngine { get; }
// For testing only
internal VisualStudioRazorParser(RazorCodeDocument codeDocument)
{
CodeDocument = codeDocument;
}
public abstract string FilePath { get; }
public VisualStudioRazorParser(
ForegroundDispatcher dispatcher,
ITextBuffer buffer,
RazorTemplateEngine templateEngine,
string filePath,
ErrorReporter errorReporter,
ICompletionBroker completionBroker,
VisualStudioDocumentTrackerFactory documentTrackerFactory,
IEditorOperationsFactoryService editorOperationsFactory)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
public abstract RazorCodeDocument CodeDocument { get; }
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
public abstract ITextSnapshot Snapshot { get; }
if (templateEngine == null)
{
throw new ArgumentNullException(nameof(templateEngine));
}
public abstract ITextBuffer TextBuffer { get; }
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}
if (errorReporter == null)
{
throw new ArgumentNullException(nameof(errorReporter));
}
if (completionBroker == null)
{
throw new ArgumentNullException(nameof(completionBroker));
}
if (documentTrackerFactory == null)
{
throw new ArgumentNullException(nameof(documentTrackerFactory));
}
if (editorOperationsFactory == null)
{
throw new ArgumentNullException(nameof(editorOperationsFactory));
}
_dispatcher = dispatcher;
TemplateEngine = templateEngine;
FilePath = filePath;
_errorReporter = errorReporter;
_textBuffer = buffer;
_completionBroker = completionBroker;
_documentTrackerFactory = documentTrackerFactory;
_textBuffer.Changed += TextBuffer_OnChanged;
_braceSmartIndenter = new BraceSmartIndenter(_dispatcher, _textBuffer, _documentTrackerFactory, editorOperationsFactory);
_parser = new BackgroundParser(templateEngine, filePath);
_parser.ResultsReady += OnResultsReady;
_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()
{
_dispatcher.AssertForegroundThread();
_textBuffer.Changed -= TextBuffer_OnChanged;
_braceSmartIndenter.Dispose();
_parser.Dispose();
StopIdleTimer();
}
// Internal for testing
internal void StartIdleTimer()
{
_dispatcher.AssertForegroundThread();
lock (IdleLock)
{
if (_idleTimer == null)
{
// Timer will fire after a fixed delay, but only once.
_idleTimer = new Timer(Timer_Tick, null, IdleDelay, Timeout.InfiniteTimeSpan);
}
}
}
// Internal for testing
internal void StopIdleTimer()
{
// Can be called from any thread.
lock (IdleLock)
{
if (_idleTimer != null)
{
_idleTimer.Dispose();
_idleTimer = null;
}
}
}
private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs args)
{
_dispatcher.AssertForegroundThread();
if (args.Changes.Count > 0)
{
// Idle timers are used to track provisional changes. Provisional changes only last for a single text change. After that normal
// partial parsing rules apply (stop the timer).
StopIdleTimer();
}
if (!args.TextChangeOccurred(out var changeInformation))
{
return;
}
var change = new SourceChange(changeInformation.firstChange.OldPosition, changeInformation.oldText.Length, changeInformation.newText);
var snapshot = args.After;
var result = PartialParseResultInternal.Rejected;
using (_parser.SynchronizeMainThreadState())
{
// Check if we can partial-parse
if (_partialParser != null && _parser.IsIdle)
{
result = _partialParser.Parse(change);
}
}
// If partial parsing failed or there were outstanding parser tasks, start a full reparse
if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
{
_parser.QueueChange(change, snapshot);
}
if ((result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional)
{
StartIdleTimer();
}
}
private void OnIdle(object state)
{
_dispatcher.AssertForegroundThread();
var documentTracker = _documentTrackerFactory.GetTracker(_textBuffer);
if (documentTracker == null)
{
Debug.Fail("Document tracker should never be null when checking idle state.");
return;
}
foreach (var textView in documentTracker.TextViews)
{
if (_completionBroker.IsCompletionActive(textView))
{
// Completion list is still active, need to re-start timer.
StartIdleTimer();
return;
}
}
Reparse();
}
private async void Timer_Tick(object state)
{
try
{
_dispatcher.AssertBackgroundThread();
StopIdleTimer();
// We need to get back to the UI thread to properly check if a completion is active.
await Task.Factory.StartNew(OnIdle, null, CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
}
catch (Exception ex)
{
// This is something totally unexpected, let's just send it over to the workspace.
await Task.Factory.StartNew(() => _errorReporter.ReportError(ex), CancellationToken.None, TaskCreationOptions.None, _dispatcher.ForegroundScheduler);
}
}
private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
{
_dispatcher.AssertBackgroundThread();
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);
}
}
public abstract Task ReparseAsync();
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.VisualStudio.Editor.Razor
{
internal abstract class VisualStudioRazorParserFactory
{
public abstract VisualStudioRazorParser Create(VisualStudioDocumentTracker documentTracker);
}
}

View File

@ -0,0 +1,53 @@
// 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.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text.Operations;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[System.Composition.Shared]
[Export(typeof(BraceSmartIndenterFactory))]
internal class DefaultBraceSmartIndenterFactory : BraceSmartIndenterFactory
{
private readonly IEditorOperationsFactoryService _editorOperationsFactory;
private readonly ForegroundDispatcher _dispatcher;
[ImportingConstructor]
public DefaultBraceSmartIndenterFactory(
IEditorOperationsFactoryService editorOperationsFactory,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (editorOperationsFactory == null)
{
throw new ArgumentNullException(nameof(editorOperationsFactory));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_editorOperationsFactory = editorOperationsFactory;
_dispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
}
public override BraceSmartIndenter Create(VisualStudioDocumentTracker documentTracker)
{
if (documentTracker == null)
{
throw new ArgumentNullException(nameof(documentTracker));
}
_dispatcher.AssertForegroundThread();
var braceSmartIndenter = new BraceSmartIndenter(_dispatcher, documentTracker, _editorOperationsFactory);
return braceSmartIndenter;
}
}
}

View File

@ -0,0 +1,141 @@
// 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.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[System.Composition.Shared]
[Export(typeof(RazorEditorFactoryService))]
internal class DefaultRazorEditorFactoryService : RazorEditorFactoryService
{
private static readonly object RazorTextBufferInitializationKey = new object();
private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory;
private readonly VisualStudioRazorParserFactory _parserFactory;
private readonly BraceSmartIndenterFactory _braceSmartIndenterFactory;
[ImportingConstructor]
public DefaultRazorEditorFactoryService(
VisualStudioDocumentTrackerFactory documentTrackerFactory,
VisualStudioRazorParserFactory parserFactory,
BraceSmartIndenterFactory braceSmartIndenterFactory)
{
if (documentTrackerFactory == null)
{
throw new ArgumentNullException(nameof(documentTrackerFactory));
}
if (parserFactory == null)
{
throw new ArgumentNullException(nameof(parserFactory));
}
if (braceSmartIndenterFactory == null)
{
throw new ArgumentNullException(nameof(braceSmartIndenterFactory));
}
_documentTrackerFactory = documentTrackerFactory;
_parserFactory = parserFactory;
_braceSmartIndenterFactory = braceSmartIndenterFactory;
}
public override bool TryGetDocumentTracker(ITextBuffer textBuffer, out VisualStudioDocumentTracker documentTracker)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (!textBuffer.IsRazorBuffer())
{
documentTracker = null;
return false;
}
EnsureTextBufferInitialized(textBuffer);
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out documentTracker))
{
Debug.Fail("Document tracker should have been stored on the text buffer during initialization.");
return false;
}
return true;
}
internal override bool TryGetParser(ITextBuffer textBuffer, out VisualStudioRazorParser parser)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (!textBuffer.IsRazorBuffer())
{
parser = null;
return false;
}
EnsureTextBufferInitialized(textBuffer);
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out parser))
{
Debug.Fail("Parser should have been stored on the text buffer during initialization.");
return false;
}
return true;
}
internal override bool TryGetSmartIndenter(ITextBuffer textBuffer, out BraceSmartIndenter braceSmartIndenter)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (!textBuffer.IsRazorBuffer())
{
braceSmartIndenter = null;
return false;
}
EnsureTextBufferInitialized(textBuffer);
if (!textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out braceSmartIndenter))
{
Debug.Fail("Brace smart indenter should have been stored on the text buffer during initialization.");
return false;
}
return true;
}
// Internal for testing
internal void EnsureTextBufferInitialized(ITextBuffer textBuffer)
{
if (textBuffer.Properties.ContainsProperty(RazorTextBufferInitializationKey))
{
// Buffer already initialized.
return;
}
var tracker = _documentTrackerFactory.Create(textBuffer);
textBuffer.Properties[typeof(VisualStudioDocumentTracker)] = tracker;
var parser = _parserFactory.Create(tracker);
textBuffer.Properties[typeof(VisualStudioRazorParser)] = parser;
var braceSmartIndenter = _braceSmartIndenterFactory.Create(tracker);
textBuffer.Properties[typeof(BraceSmartIndenter)] = braceSmartIndenter;
textBuffer.Properties.AddProperty(RazorTextBufferInitializationKey, RazorTextBufferInitializationKey);
}
}
}

View File

@ -3,10 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -19,12 +15,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker
{
private readonly string _filePath;
private readonly ProjectSnapshotManager _projectManager;
private readonly TextBufferProjectService _projectService;
private readonly ITextBuffer _textBuffer;
private readonly List<ITextView> _textViews;
private readonly Workspace _workspace;
private bool _isSupportedProject;
private ProjectSnapshot _project;
private string _projectPath;
@ -32,11 +28,17 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public override event EventHandler ContextChanged;
public DefaultVisualStudioDocumentTracker(
string filePath,
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
Workspace workspace,
ITextBuffer textBuffer)
{
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
@ -57,6 +59,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
throw new ArgumentNullException(nameof(textBuffer));
}
_filePath = filePath;
_projectManager = projectManager;
_projectService = projectService;
_textBuffer = textBuffer;
@ -75,10 +78,48 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public override IReadOnlyList<ITextView> TextViews => _textViews;
public IList<ITextView> TextViewsInternal => _textViews;
public override Workspace Workspace => _workspace;
public override string FilePath => _filePath;
public override string ProjectPath => _projectPath;
internal void AddTextView(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (!_textViews.Contains(textView))
{
_textViews.Add(textView);
if (_textViews.Count == 1)
{
Subscribe();
}
}
}
internal void RemoveTextView(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (_textViews.Contains(textView))
{
_textViews.Remove(textView);
if (_textViews.Count == 0)
{
Unsubscribe();
}
}
}
public override ITextView GetFocusedTextView()
{
for (var i = 0; i < TextViews.Count; i++)
@ -92,7 +133,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return null;
}
public void Subscribe()
private void Subscribe()
{
// Fundamentally we have a Razor half of the world as as soon as the document is open - and then later
// the C# half of the world will be initialized. This code is in general pretty tolerant of
@ -126,61 +167,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
OnContextChanged(_project);
}
public void Unsubscribe()
private void Unsubscribe()
{
_projectManager.Changed -= ProjectManager_Changed;
// Detached from project.
_isSupportedProject = false;
_project = null;
OnContextChanged(project: null);
}
private void OnContextChanged(ProjectSnapshot project)
{
_project = project;
// Hack: When the context changes we want to replace the template engine held by the parser.
// This code isn't super well factored now - it's intended to be limited to one spot until
// we have time to a proper redesign.
if (TextBuffer.Properties.TryGetProperty(typeof(RazorEditorParser), out RazorEditorParser legacyParser) &&
legacyParser.TemplateEngine != null &&
_projectPath != null)
{
var factory = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<CodeAnalysis.Razor.RazorTemplateEngineFactoryService>();
var existingEngine = legacyParser.TemplateEngine;
var projectDirectory = Path.GetDirectoryName(_projectPath);
var templateEngine = factory.Create(projectDirectory, builder =>
{
var existingVSParserOptions = existingEngine.Engine.Features.FirstOrDefault(
feature => string.Equals(
feature.GetType().Name,
"VisualStudioParserOptionsFeature",
StringComparison.Ordinal));
if (existingVSParserOptions == null)
{
Debug.Fail("The VS Parser options should have been set.");
}
else
{
builder.Features.Add(existingVSParserOptions);
}
var existingTagHelperFeature = existingEngine.Engine.Features
.OfType<ITagHelperFeature>()
.FirstOrDefault();
if (existingTagHelperFeature == null)
{
Debug.Fail("The VS TagHelperFeature should have been set.");
}
else
{
builder.Features.Add(existingTagHelperFeature);
}
});
legacyParser.TemplateEngine = templateEngine;
}
var handler = ContextChanged;
if (handler != null)
{

View File

@ -2,34 +2,30 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[ContentType(RazorLanguage.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Document)]
[Export(typeof(IWpfTextViewConnectionListener))]
[System.Composition.Shared]
[Export(typeof(VisualStudioDocumentTrackerFactory))]
internal class DefaultVisualStudioDocumentTrackerFactory : VisualStudioDocumentTrackerFactory, IWpfTextViewConnectionListener
internal class DefaultVisualStudioDocumentTrackerFactory : VisualStudioDocumentTrackerFactory
{
private readonly TextBufferProjectService _projectService;
private readonly ITextDocumentFactoryService _textDocumentFactory;
private readonly Workspace _workspace;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ProjectSnapshotManager _projectManager;
private readonly TextBufferProjectService _projectService;
private readonly Workspace _workspace;
[ImportingConstructor]
public DefaultVisualStudioDocumentTrackerFactory(
TextBufferProjectService projectService,
ITextDocumentFactoryService textDocumentFactory,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (projectService == null)
@ -37,181 +33,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
throw new ArgumentNullException(nameof(projectService));
}
if (textDocumentFactory == null)
{
throw new ArgumentNullException(nameof(textDocumentFactory));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_projectService = projectService;
_textDocumentFactory = textDocumentFactory;
_workspace = workspace;
_foregroundDispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
_projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>();
}
// This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests.
internal DefaultVisualStudioDocumentTrackerFactory(
ForegroundDispatcher foregroundDispatcher,
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_foregroundDispatcher = foregroundDispatcher;
_projectManager = projectManager;
_projectService = projectService;
_workspace = workspace;
}
public Workspace Workspace => _workspace;
public override VisualStudioDocumentTracker GetTracker(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
_foregroundDispatcher.AssertForegroundThread();
// While it's definitely possible to have multiple Razor text buffers attached to the same text view, there's
// no real scenario for it. This method always returns the tracker for the first Razor text buffer, but the
// other functionality for this class will maintain state correctly for the other buffers.
var textBuffer = textView.BufferGraph.GetRazorBuffers().FirstOrDefault();
if (textBuffer == null)
{
// No Razor buffer, nothing to track.
return null;
}
// A little bit of hardening here, to make sure our assumptions are correct.
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
Debug.Fail("The document tracker should be initialized");
}
Debug.Assert(tracker.TextViewsInternal.Contains(textView));
return tracker;
}
public override VisualStudioDocumentTracker GetTracker(ITextBuffer textBuffer)
public override VisualStudioDocumentTracker Create(ITextBuffer textBuffer)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
_foregroundDispatcher.AssertForegroundThread();
if (!textBuffer.IsRazorBuffer())
if (!_textDocumentFactory.TryGetTextDocument(textBuffer, out var textDocument))
{
// Not a Razor buffer.
Debug.Fail("Text document should be available from the text buffer.");
return null;
}
// A little bit of hardening here, to make sure our assumptions are correct.
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
Debug.Fail("The document tracker should be initialized");
}
var filePath = textDocument.FilePath;
var tracker = new DefaultVisualStudioDocumentTracker(filePath, _projectManager, _projectService, _workspace, textBuffer);
return tracker;
}
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_foregroundDispatcher.AssertForegroundThread();
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
if (!textBuffer.IsRazorBuffer())
{
continue;
}
DefaultVisualStudioDocumentTracker tracker;
if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
tracker = new DefaultVisualStudioDocumentTracker(_projectManager, _projectService, _workspace, textBuffer);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
}
if (!tracker.TextViewsInternal.Contains(textView))
{
tracker.TextViewsInternal.Add(textView);
if (tracker.TextViewsInternal.Count == 1)
{
tracker.Subscribe();
}
}
}
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_foregroundDispatcher.AssertForegroundThread();
// This means a Razor buffer has be detached from this ITextView or the ITextView is closing. Since we keep a
// list of all of the open text views for each text buffer, we need to update the tracker.
//
// Notice that this method is called *after* changes are applied to the text buffer(s). We need to check every
// one of them for a tracker because the content type could have changed.
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
DefaultVisualStudioDocumentTracker tracker;
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{
tracker.TextViewsInternal.Remove(textView);
if (tracker.TextViewsInternal.Count == 0)
{
tracker.Unsubscribe();
}
}
}
}
}
}

View File

@ -0,0 +1,73 @@
// 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.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
using TemplateEngineFactoryService = Microsoft.CodeAnalysis.Razor.RazorTemplateEngineFactoryService;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[System.Composition.Shared]
[Export(typeof(VisualStudioRazorParserFactory))]
internal class DefaultVisualStudioRazorParserFactory : VisualStudioRazorParserFactory
{
private readonly ForegroundDispatcher _dispatcher;
private readonly TemplateEngineFactoryService _templateEngineFactoryService;
private readonly ICompletionBroker _completionBroker;
private readonly IEnumerable<IContextChangedListener> _parserContextChangedListeners;
private readonly ErrorReporter _errorReporter;
[ImportingConstructor]
public DefaultVisualStudioRazorParserFactory(
ICompletionBroker completionBroker,
[ImportMany(typeof(IContextChangedListener))] IEnumerable<IContextChangedListener> parserContextChangedListeners,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (completionBroker == null)
{
throw new ArgumentNullException(nameof(completionBroker));
}
if (parserContextChangedListeners == null)
{
throw new ArgumentNullException(nameof(parserContextChangedListeners));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_completionBroker = completionBroker;
_parserContextChangedListeners = parserContextChangedListeners;
_dispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
_errorReporter = workspace.Services.GetRequiredService<ErrorReporter>();
var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name);
_templateEngineFactoryService = razorLanguageServices.GetRequiredService<TemplateEngineFactoryService>();
}
public override VisualStudioRazorParser Create(VisualStudioDocumentTracker documentTracker)
{
if (documentTracker == null)
{
throw new ArgumentNullException(nameof(documentTracker));
}
_dispatcher.AssertForegroundThread();
var parser = new DefaultVisualStudioRazorParser(
_dispatcher,
documentTracker,
_templateEngineFactoryService,
_errorReporter,
_completionBroker,
_parserContextChangedListeners);
return parser;
}
}
}

View File

@ -0,0 +1,139 @@
// 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.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[ContentType(RazorLanguage.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Document)]
[Export(typeof(IWpfTextViewConnectionListener))]
internal class RazorTextViewConnectionListener : IWpfTextViewConnectionListener
{
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly RazorEditorFactoryService _editorFactoryService;
private readonly Workspace _workspace;
[ImportingConstructor]
public RazorTextViewConnectionListener(
RazorEditorFactoryService editorFactoryService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_editorFactoryService = editorFactoryService;
_workspace = workspace;
_foregroundDispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
}
// This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests.
internal RazorTextViewConnectionListener(
ForegroundDispatcher foregroundDispatcher,
RazorEditorFactoryService editorFactoryService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_foregroundDispatcher = foregroundDispatcher;
_editorFactoryService = editorFactoryService;
_workspace = workspace;
}
public Workspace Workspace => _workspace;
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_foregroundDispatcher.AssertForegroundThread();
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
if (!textBuffer.IsRazorBuffer())
{
continue;
}
if (!_editorFactoryService.TryGetDocumentTracker(textBuffer, out var documentTracker) ||
!(documentTracker is DefaultVisualStudioDocumentTracker tracker))
{
Debug.Fail("Tracker should always be available given our expectations of the VS workflow.");
return;
}
tracker.AddTextView(textView);
}
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_foregroundDispatcher.AssertForegroundThread();
// This means a Razor buffer has be detached from this ITextView or the ITextView is closing. Since we keep a
// list of all of the open text views for each text buffer, we need to update the tracker.
//
// Notice that this method is called *after* changes are applied to the text buffer(s). We need to check every
// one of them for a tracker because the content type could have changed.
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
DefaultVisualStudioDocumentTracker documentTracker;
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out documentTracker))
{
documentTracker.RemoveTextView(textView);
}
}
}
}
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
var editorOperationsFactory = CreateOperationsFactoryService();
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory);
// Act
textBuffer.ApplyEdit(edit);
@ -51,7 +51,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
var editorOperationsFactory = CreateOperationsFactoryService();
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory);
// Act
textBuffer.ApplyEdit(edit);
@ -76,7 +76,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var documentTracker = CreateDocumentTracker(() => textBuffer, focusedTextView);
textBuffer = CreateTextBuffer(initialSnapshot, documentTracker);
var editorOperationsFactory = CreateOperationsFactoryService();
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, CreateDocumentTrackerFactory(() => textBuffer, documentTracker), editorOperationsFactory);
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory);
// Act
textBuffer.ApplyEdit(edit);

View File

@ -65,9 +65,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
var editorOperations = new Mock<IEditorOperations>();
editorOperations.Setup(operations => operations.MoveToEndOfLine(false));
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
var documentTracker = CreateDocumentTracker(() => Mock.Of<ITextBuffer>(), textView);
editorOperationsFactory.Setup(factory => factory.GetEditorOperations(textView))
.Returns(editorOperations.Object);
var smartIndenter = new BraceSmartIndenter(Dispatcher, new Mock<ITextBuffer>().Object, new Mock<VisualStudioDocumentTrackerFactory>().Object, editorOperationsFactory.Object);
var smartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory.Object);
// Act
smartIndenter.TriggerSmartIndent(textView);
@ -136,12 +137,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void TextBuffer_OnChanged_NoopsIfNoChanges()
{
// Arrange
var textBuffer = new Mock<ITextBuffer>();
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
var changeCollection = new TestTextChangeCollection();
var textContentChangeArgs = new TestTextContentChangedEventArgs(changeCollection);
var documentTrackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer.Object, documentTrackerFactory.Object, editorOperationsFactory.Object);
var documentTracker = CreateDocumentTracker(() => Mock.Of<ITextBuffer>(), Mock.Of<ITextView>());
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory.Object);
// Act & Assert
braceSmartIndenter.TextBuffer_OnChanged(null, textContentChangeArgs);
@ -155,8 +155,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
var textBuffer = new TestTextBuffer(initialSnapshot);
var edit = new TestEdit(0, 0, initialSnapshot, 0, initialSnapshot, string.Empty);
var editorOperationsFactory = new Mock<IEditorOperationsFactoryService>();
var documentTrackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, documentTrackerFactory.Object, editorOperationsFactory.Object);
var documentTracker = CreateDocumentTracker(() => textBuffer, Mock.Of<ITextView>());
var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory.Object);
// Act & Assert
textBuffer.ApplyEdits(edit, edit);

View File

@ -24,15 +24,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
return tracker.Object;
}
protected static VisualStudioDocumentTrackerFactory CreateDocumentTrackerFactory(Func<ITextBuffer> bufferAccessor, VisualStudioDocumentTracker documentTracker)
{
var trackerFactory = new Mock<VisualStudioDocumentTrackerFactory>();
trackerFactory.Setup(factory => factory.GetTracker(It.IsAny<ITextBuffer>()))
.Returns(documentTracker);
return trackerFactory.Object;
}
protected static ITextView CreateFocusedTextView(Func<ITextBuffer> textBufferAccessor = null, ITextCaret caret = null)
{
var focusedTextView = new Mock<ITextView>();

View File

@ -1,19 +1,16 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Moq;
using Xunit;
using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;
using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using System.Collections.Generic;
using Moq;
using System;
namespace Microsoft.VisualStudio.Editor.Razor
{
@ -197,12 +194,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(Workspace workspace)
public TestProjectSnapshotManager(Workspace workspace)
: base(
Mock.Of<ForegroundDispatcher>(),
Mock.Of<ErrorReporter>(),
Mock.Of<ProjectSnapshotWorker>(),
Enumerable.Empty<ProjectSnapshotChangeTrigger>(),
Mock.Of<ForegroundDispatcher>(),
Mock.Of<ErrorReporter>(),
Mock.Of<ProjectSnapshotWorker>(),
Enumerable.Empty<ProjectSnapshotChangeTrigger>(),
workspace)
{
}

View File

@ -3,7 +3,6 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
@ -12,38 +11,51 @@ namespace Microsoft.VisualStudio.Editor.Razor
public class DefaultTextBufferCodeDocumentProviderTest
{
[Fact]
public void TryGetFromBuffer_UsesVisualStudioRazorParserIfAvailable()
public void TryGetFromBuffer_SucceedsIfParserFromProviderHasCodeDocument()
{
// Arrange
var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World");
var parser = new VisualStudioRazorParser(expectedCodeDocument);
var properties = new PropertyCollection();
properties.AddProperty(typeof(VisualStudioRazorParser), parser);
var textBuffer = new Mock<ITextBuffer>();
textBuffer.Setup(buffer => buffer.Properties)
.Returns(properties);
var provider = new DefaultTextBufferCodeDocumentProvider();
VisualStudioRazorParser parser = new DefaultVisualStudioRazorParser(expectedCodeDocument);
var parserProvider = Mock.Of<RazorEditorFactoryService>(p => p.TryGetParser(It.IsAny<ITextBuffer>(), out parser) == true);
var textBuffer = Mock.Of<ITextBuffer>();
var provider = new DefaultTextBufferCodeDocumentProvider(parserProvider);
// Act
var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument);
var result = provider.TryGetFromBuffer(textBuffer, out var codeDocument);
// Assert
Assert.True(result);
Assert.Same(expectedCodeDocument, codeDocument);
}
[Fact]
public void TryGetFromBuffer_FailsIfParserFromProviderMissingCodeDocument()
{
// Arrange
VisualStudioRazorParser parser = new DefaultVisualStudioRazorParser(codeDocument: null);
var parserProvider = Mock.Of<RazorEditorFactoryService>(p => p.TryGetParser(It.IsAny<ITextBuffer>(), out parser) == true);
var textBuffer = Mock.Of<ITextBuffer>();
var provider = new DefaultTextBufferCodeDocumentProvider(parserProvider);
// Act
var result = provider.TryGetFromBuffer(textBuffer, out var codeDocument);
// Assert
Assert.False(result);
Assert.Null(codeDocument);
}
[Fact]
public void TryGetFromBuffer_FailsIfNoParserIsAvailable()
{
// Arrange
var properties = new PropertyCollection();
var textBuffer = new Mock<ITextBuffer>();
textBuffer.Setup(buffer => buffer.Properties)
.Returns(properties);
var provider = new DefaultTextBufferCodeDocumentProvider();
VisualStudioRazorParser parser = null;
var parserProvider = Mock.Of<RazorEditorFactoryService>(p => p.TryGetParser(It.IsAny<ITextBuffer>(), out parser) == false);
var textBuffer = Mock.Of<ITextBuffer>();
var provider = new DefaultTextBufferCodeDocumentProvider(parserProvider);
// Act
var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument);
var result = provider.TryGetFromBuffer(textBuffer, out var codeDocument);
// Assert
Assert.False(result);

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
@ -14,63 +16,32 @@ using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class VisualStudioRazorParserTest : ForegroundDispatcherTestBase
public class DefaultVisualStudioRazorParserIntegrationTest : ForegroundDispatcherTestBase
{
private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
private const string TestProjectPath = "C:\\This\\Path\\Is\\Just\\For\\Project.csproj";
[Fact]
public void ConstructorRequiresNonNullPhysicalPath()
{
Assert.Throws<ArgumentException>("filePath",
() => new VisualStudioRazorParser(
Dispatcher,
new TestTextBuffer(null),
CreateTemplateEngine(),
null,
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().Object));
}
[Fact]
public void ConstructorRequiresNonEmptyPhysicalPath()
{
Assert.Throws<ArgumentException>("filePath",
() => new VisualStudioRazorParser(
Dispatcher,
new TestTextBuffer(null),
CreateTemplateEngine(),
string.Empty,
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().Object));
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
[ForegroundFact]
public void BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
{
// Arrange
var original = new StringTextSnapshot("Foo @bar Baz");
var testBuffer = new TestTextBuffer(original);
using (var parser = new VisualStudioRazorParser(
Dispatcher,
testBuffer,
CreateTemplateEngine(),
TestLinePragmaFileName,
var documentTracker = CreateDocumentTracker(testBuffer);
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
CreateTemplateEngineFactory(),
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().Object))
Enumerable.Empty<IContextChangedListener>()))
{
parser.IdleDelay = TimeSpan.FromMilliseconds(100);
parser.DocumentTracker_ContextChanged(null, null);
var changed = new StringTextSnapshot("Foo @bap Daz");
var edit = new TestEdit(7, 3, original, 3, changed, "p D");
var parseComplete = new ManualResetEventSlim();
@ -98,8 +69,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void AwaitPeriodInsertionAcceptedProvisionally()
[ForegroundFact]
public async Task AwaitPeriodInsertionAcceptedProvisionally()
{
// Arrange
var original = new StringTextSnapshot("foo @await Html baz");
@ -111,7 +82,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
manager.InitializeWithDocument(edit.OldSnapshot);
// Act
manager.ApplyEditAndWaitForReparse(edit);
await manager.ApplyEditAndWaitForReparseAsync(edit);
// Assert
Assert.Equal(2, manager.ParseCount);
@ -124,8 +95,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
[ForegroundFact]
public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
@ -183,8 +154,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock()
[ForegroundFact]
public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
@ -234,8 +205,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions()
[ForegroundFact]
public async Task ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @DateT. baz");
@ -268,7 +239,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
ApplyAndVerifyPartialChange(edit, "DateTime.");
// Verify the reparse finally comes
manager.WaitForReparse();
await manager.WaitForReparseAsync();
Assert.Equal(2, manager.ParseCount);
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
@ -280,8 +251,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers()
[ForegroundFact]
public async Task ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @DateTime. baz");
@ -320,7 +291,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
ApplyAndVerifyPartialChange(edit, "DateTime.Now.");
// Verify the reparse eventually happens
manager.WaitForReparse();
await manager.WaitForReparseAsync();
Assert.Equal(2, manager.ParseCount);
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
@ -332,8 +303,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
[ForegroundFact]
public async Task ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
{
var factory = new SpanFactory();
var original = new StringTextSnapshot("foo @date baz");
@ -383,7 +354,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "DateTime.");
// Verify the reparse eventually happens
manager.WaitForReparse();
await manager.WaitForReparseAsync();
Assert.Equal(2, manager.ParseCount);
ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
@ -395,8 +366,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
[ForegroundFact]
public async Task ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
{
// Arrange
var factory = new SpanFactory();
@ -407,7 +378,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
manager.InitializeWithDocument(dotTyped.OldSnapshot);
// Apply the dot change
manager.ApplyEditAndWaitForReparse(dotTyped);
await manager.ApplyEditAndWaitForReparseAsync(dotTyped);
// Act (apply the identifier start char change)
manager.ApplyEditAndWaitForParse(charTyped);
@ -432,8 +403,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
[ForegroundFact]
public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
{
// Arrange
var factory = new SpanFactory();
@ -463,106 +434,115 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
{
RunTypeKeywordTest("if");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
{
RunTypeKeywordTest("do");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
{
RunTypeKeywordTest("try");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
{
RunTypeKeywordTest("for");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped()
{
RunTypeKeywordTest("foreach");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
{
RunTypeKeywordTest("while");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped()
{
RunTypeKeywordTest("switch");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped()
{
RunTypeKeywordTest("lock");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
{
RunTypeKeywordTest("using");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
{
RunTypeKeywordTest("section");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
{
RunTypeKeywordTest("inherits");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
{
RunTypeKeywordTest("functions");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
{
RunTypeKeywordTest("namespace");
}
// [Fact] Silent skip to avoid warnings. Skipping until we can control the parser more directly.
private void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
[ForegroundFact]
public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
{
RunTypeKeywordTest("class");
}
private TestParserManager CreateParserManager(ITextSnapshot originalSnapshot, int idleDelay = 50)
private TestParserManager CreateParserManager(ITextSnapshot originalSnapshot)
{
var parser = new VisualStudioRazorParser(
Dispatcher,
new TestTextBuffer(originalSnapshot),
CreateTemplateEngine(),
TestLinePragmaFileName,
var textBuffer = new TestTextBuffer(originalSnapshot);
var documentTracker = CreateDocumentTracker(textBuffer);
var templateEngineFactory = CreateTemplateEngineFactory();
var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
templateEngineFactory,
new DefaultErrorReporter(),
new TestCompletionBroker(),
new Mock<VisualStudioDocumentTrackerFactory>().Object,
new Mock<IEditorOperationsFactoryService>().Object);
new TestCompletionBroker(),
Enumerable.Empty<IContextChangedListener>())
{
// We block idle work with the below reset events. Therefore, make tests fast and have the idle timer fire as soon as possible.
IdleDelay = TimeSpan.FromMilliseconds(1),
NotifyForegroundIdleStart = new ManualResetEventSlim(),
BlockBackgroundIdleWork = new ManualResetEventSlim(),
};
parser.StartParser();
return new TestParserManager(parser);
}
private static RazorTemplateEngine CreateTemplateEngine(
private static RazorTemplateEngineFactoryService CreateTemplateEngineFactory(
string path = TestLinePragmaFileName,
IEnumerable<TagHelperDescriptor> tagHelpers = null)
{
@ -585,7 +565,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
var templateEngine = new RazorTemplateEngine(engine, project);
templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml");
return templateEngine;
var templateEngineFactory = Mock.Of<RazorTemplateEngineFactoryService>(
service => service.Create(It.IsAny<string>(), It.IsAny<Action<IRazorEngineBuilder>>()) == templateEngine);
return templateEngineFactory;
}
private void RunTypeKeywordTest(string keyword)
@ -619,12 +603,26 @@ namespace Microsoft.VisualStudio.Editor.Razor
else
{
#endif
Assert.True(withTimeout((int)TimeSpan.FromSeconds(1).TotalMilliseconds), "Timeout expired!");
Assert.True(withTimeout((int)TimeSpan.FromSeconds(5).TotalMilliseconds), "Timeout expired!");
#if DEBUG
}
#endif
}
private static VisualStudioDocumentTracker CreateDocumentTracker(Text.ITextBuffer textBuffer)
{
var focusedTextView = Mock.Of<ITextView>(textView => textView.HasAggregateFocus == true);
var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
tracker.TextBuffer == textBuffer &&
tracker.TextViews == new[] { focusedTextView } &&
tracker.FilePath == TestLinePragmaFileName &&
tracker.ProjectPath == TestProjectPath &&
tracker.IsSupportedProject == true);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);
return documentTracker;
}
private class TestParserManager : IDisposable
{
public int ParseCount;
@ -632,22 +630,22 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly ManualResetEventSlim _parserComplete;
private readonly ManualResetEventSlim _reparseComplete;
private readonly TestTextBuffer _testBuffer;
private readonly VisualStudioRazorParser _parser;
private readonly DefaultVisualStudioRazorParser _parser;
public TestParserManager(VisualStudioRazorParser parser)
public TestParserManager(DefaultVisualStudioRazorParser parser)
{
_parserComplete = new ManualResetEventSlim();
_reparseComplete = new ManualResetEventSlim();
_testBuffer = (TestTextBuffer)parser._textBuffer;
_testBuffer = (TestTextBuffer)parser.TextBuffer;
ParseCount = 0;
// Change idle delay to be huge in order to enable us to take control of when idle methods fire.
parser.IdleDelay = TimeSpan.FromMinutes(2);
_parser = parser;
parser.DocumentStructureChanged += (sender, args) =>
{
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
Interlocked.Increment(ref ParseCount);
_parserComplete.Set();
if (args.SourceChange == null)
{
@ -655,7 +653,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
_reparseComplete.Set();
}
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
_parserComplete.Set();
};
}
@ -680,10 +678,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
WaitForParse();
}
public void ApplyEditAndWaitForReparse(TestEdit edit)
public async Task ApplyEditAndWaitForReparseAsync(TestEdit edit)
{
ApplyEdit(edit);
WaitForReparse();
await WaitForReparseAsync();
}
public void WaitForParse()
@ -692,17 +690,26 @@ namespace Microsoft.VisualStudio.Editor.Razor
_parserComplete.Reset();
}
public void WaitForReparse()
public async Task WaitForReparseAsync()
{
Assert.True(_parser._idleTimer != null, "Expected the parser to be waiting for an idle invocation but it was not.");
Assert.True(_parser._idleTimer != null);
// Allow background idle work to continue
_parser.BlockBackgroundIdleWork.Set();
// Get off of the foreground thread so we can wait for the idle timer to fire
await Task.Run(() =>
{
DoWithTimeoutIfNotDebugging(_parser.NotifyForegroundIdleStart.Wait);
});
_parser.StopIdleTimer();
_parser.IdleDelay = TimeSpan.FromMilliseconds(50);
_parser.StartIdleTimer();
DoWithTimeoutIfNotDebugging(_reparseComplete.Wait);
_reparseComplete.Reset();
Assert.Null(_parser._idleTimer);
_parser.IdleDelay = TimeSpan.FromMinutes(2);
DoWithTimeoutIfNotDebugging(_reparseComplete.Wait);
_reparseComplete.Reset();
_parser.BlockBackgroundIdleWork.Reset();
_parser.NotifyForegroundIdleStart.Reset();
}
public void Dispose()

View File

@ -0,0 +1,201 @@
// 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.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultVisualStudioRazorParserTest : ForegroundDispatcherTestBase
{
private static VisualStudioDocumentTracker CreateDocumentTracker(bool isSupportedProject = true)
{
var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
tracker.TextBuffer == new TestTextBuffer(new StringTextSnapshot(string.Empty)) &&
tracker.ProjectPath == "SomeProject.csproj" &&
tracker.FilePath == "SomeFilePath.cshtml" &&
tracker.IsSupportedProject == isSupportedProject);
return documentTracker;
}
[ForegroundFact]
public void StartIdleTimer_DoesNotRestartTimerWhenAlreadyRunning()
{
// Arrange
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>())
{
BlockBackgroundIdleWork = new ManualResetEventSlim(),
IdleDelay = TimeSpan.FromSeconds(5)
})
{
parser.StartIdleTimer();
using (var currentTimer = parser._idleTimer)
{
// Act
parser.StartIdleTimer();
var afterTimer = parser._idleTimer;
// Assert
Assert.NotNull(currentTimer);
Assert.Same(currentTimer, afterTimer);
}
}
}
[ForegroundFact]
public void StopIdleTimer_StopsTimer()
{
// Arrange
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>())
{
BlockBackgroundIdleWork = new ManualResetEventSlim(),
IdleDelay = TimeSpan.FromSeconds(5)
})
{
parser.StartIdleTimer();
var currentTimer = parser._idleTimer;
// Act
parser.StopIdleTimer();
// Assert
Assert.NotNull(currentTimer);
Assert.Null(parser._idleTimer);
}
}
[ForegroundFact]
public void StopParser_DetachesFromTextBufferChangeLoop()
{
// Arrange
var documentTracker = CreateDocumentTracker();
var textBuffer = (TestTextBuffer)documentTracker.TextBuffer;
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>()))
{
parser.StartParser();
// Act
parser.StopParser();
// Assert
Assert.Empty(textBuffer.AttachedChangedEvents);
Assert.Null(parser._parser);
}
}
[ForegroundFact]
public void StartParser_AttachesToTextBufferChangeLoop()
{
// Arrange
var documentTracker = CreateDocumentTracker();
var textBuffer = (TestTextBuffer)documentTracker.TextBuffer;
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>()))
{
// Act
parser.StartParser();
// Assert
Assert.Equal(1, textBuffer.AttachedChangedEvents.Count);
Assert.NotNull(parser._parser);
}
}
[ForegroundFact]
public void NotifyParserContextChanged_NotifiesListeners()
{
// Arrange
var listener1 = new Mock<IContextChangedListener>();
listener1.Setup(l => l.OnContextChanged(It.IsAny<VisualStudioRazorParser>()));
var listener2 = new Mock<IContextChangedListener>();
listener2.Setup(l => l.OnContextChanged(It.IsAny<VisualStudioRazorParser>()));
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
new[] { listener1.Object, listener2.Object }))
{
// Act
parser.NotifyParserContextChanged();
// Assert
listener1.Verify();
listener2.Verify();
}
}
[ForegroundFact]
public void TryReinitializeParser_ReturnsTrue_IfProjectIsSupported()
{
// Arrange
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(isSupportedProject: true),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>()))
{
// Act
var result = parser.TryReinitializeParser();
// Assert
Assert.True(result);
}
}
[ForegroundFact]
public void TryReinitializeParser_ReturnsFalse_IfProjectIsNotSupported()
{
// Arrange
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(isSupportedProject: false),
Mock.Of<RazorTemplateEngineFactoryService>(),
new DefaultErrorReporter(),
Mock.Of<ICompletionBroker>(),
Enumerable.Empty<IContextChangedListener>()))
{
// Act
var result = parser.TryReinitializeParser();
// Assert
Assert.False(result);
}
}
}
}

View File

@ -11,10 +11,13 @@ namespace Microsoft.VisualStudio.Test
public class TestTextBuffer : ITextBuffer
{
private ITextSnapshot _currentSnapshot;
private List<EventHandler<TextContentChangedEventArgs>> _attachedChangedEvents;
public TestTextBuffer(ITextSnapshot initialSnapshot)
{
_currentSnapshot = initialSnapshot;
_attachedChangedEvents = new List<EventHandler<TextContentChangedEventArgs>>();
ReadOnlyRegionsChanged += (sender, args) => { };
ChangedLowPriority += (sender, args) => { };
ChangedHighPriority += (sender, args) => { };
@ -39,7 +42,11 @@ namespace Microsoft.VisualStudio.Test
_currentSnapshot = edits[edits.Length - 1].NewSnapshot;
Changed?.Invoke(this, args);
foreach (var changedEvent in AttachedChangedEvents)
{
changedEvent.Invoke(this, args);
}
PostChanged?.Invoke(null, null);
ReadOnlyRegionsChanged?.Invoke(null, null);
@ -49,12 +56,27 @@ namespace Microsoft.VisualStudio.Test
ContentTypeChanged?.Invoke(null, null);
}
public IReadOnlyList<EventHandler<TextContentChangedEventArgs>> AttachedChangedEvents => _attachedChangedEvents;
public ITextSnapshot CurrentSnapshot => _currentSnapshot;
public PropertyCollection Properties { get; }
public event EventHandler<SnapshotSpanEventArgs> ReadOnlyRegionsChanged;
public event EventHandler<TextContentChangedEventArgs> Changed;
public event EventHandler<TextContentChangedEventArgs> Changed
{
add
{
_attachedChangedEvents.Add(value);
}
remove
{
_attachedChangedEvents.Remove(value);
}
}
public event EventHandler<TextContentChangedEventArgs> ChangedLowPriority;
public event EventHandler<TextContentChangedEventArgs> ChangedHighPriority;
public event EventHandler<TextContentChangingEventArgs> Changing;

View File

@ -0,0 +1,237 @@
// 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.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultRazorEditorFactoryServiceTest
{
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
[Fact]
public void TryGetDocumentTracker_ForRazorTextBuffer_ReturnsTrue()
{
// Arrange
var expectedDocumentTracker = Mock.Of<VisualStudioDocumentTracker>();
var factoryService = CreateFactoryService(expectedDocumentTracker);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetDocumentTracker(textBuffer, out var documentTracker);
// Assert
Assert.True(result);
Assert.Same(expectedDocumentTracker, documentTracker);
}
[Fact]
public void TryGetDocumentTracker_NonRazorBuffer_ReturnsFalse()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetDocumentTracker(textBuffer, out var documentTracker);
// Assert
Assert.False(result);
Assert.Null(documentTracker);
}
[Fact]
public void EnsureTextBufferInitialized_StoresTracker()
{
// Arrange
var expectedDocumentTracker = Mock.Of<VisualStudioDocumentTracker>();
var factoryService = CreateFactoryService(expectedDocumentTracker);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out VisualStudioDocumentTracker documentTracker));
Assert.Same(expectedDocumentTracker, documentTracker);
}
[Fact]
public void EnsureTextBufferInitialized_OnlyStoresTrackerOnTextBufferOnce()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
factoryService.EnsureTextBufferInitialized(textBuffer);
var expectedDocumentTracker = textBuffer.Properties[typeof(VisualStudioDocumentTracker)];
// Create a second factory service so it generates a different tracker
factoryService = CreateFactoryService();
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out VisualStudioDocumentTracker documentTracker));
Assert.Same(expectedDocumentTracker, documentTracker);
}
[Fact]
public void TryGetParser_ForRazorTextBuffer_ReturnsTrue()
{
// Arrange
var expectedParser = Mock.Of<VisualStudioRazorParser>();
var factoryService = CreateFactoryService(parser: expectedParser);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetParser(textBuffer, out var parser);
// Assert
Assert.True(result);
Assert.Same(expectedParser, parser);
}
[Fact]
public void TryGetParser_NonRazorBuffer_ReturnsFalse()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetParser(textBuffer, out var parser);
// Assert
Assert.False(result);
Assert.Null(parser);
}
[Fact]
public void EnsureTextBufferInitialized_StoresParser()
{
// Arrange
var expectedParser = Mock.Of<VisualStudioRazorParser>();
var factoryService = CreateFactoryService(parser: expectedParser);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser));
Assert.Same(expectedParser, parser);
}
[Fact]
public void EnsureTextBufferInitialized_OnlyStoresParserOnTextBufferOnce()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
factoryService.EnsureTextBufferInitialized(textBuffer);
var expectedParser = textBuffer.Properties[typeof(VisualStudioRazorParser)];
// Create a second factory service so it generates a different parser
factoryService = CreateFactoryService();
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser));
Assert.Same(expectedParser, parser);
}
[Fact]
public void TryGetSmartIndenter_ForRazorTextBuffer_ReturnsTrue()
{
// Arrange
var expectedSmartIndenter = Mock.Of<BraceSmartIndenter>();
var factoryService = CreateFactoryService(smartIndenter: expectedSmartIndenter);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetSmartIndenter(textBuffer, out var smartIndenter);
// Assert
Assert.True(result);
Assert.Same(expectedSmartIndenter, smartIndenter);
}
[Fact]
public void TryGetSmartIndenter_NonRazorBuffer_ReturnsFalse()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factoryService.TryGetSmartIndenter(textBuffer, out var smartIndenter);
// Assert
Assert.False(result);
Assert.Null(smartIndenter);
}
[Fact]
public void EnsureTextBufferInitialized_StoresSmartIndenter()
{
// Arrange
var expectedSmartIndenter = Mock.Of<BraceSmartIndenter>();
var factoryService = CreateFactoryService(smartIndenter: expectedSmartIndenter);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out BraceSmartIndenter smartIndenter));
Assert.Same(expectedSmartIndenter, smartIndenter);
}
[Fact]
public void EnsureTextBufferInitialized_OnlyStoresSmartIndenterOnTextBufferOnce()
{
// Arrange
var factoryService = CreateFactoryService();
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
factoryService.EnsureTextBufferInitialized(textBuffer);
var expectedSmartIndenter = textBuffer.Properties[typeof(BraceSmartIndenter)];
// Create a second factory service so it generates a different smart indenter
factoryService = CreateFactoryService();
// Act
factoryService.EnsureTextBufferInitialized(textBuffer);
// Assert
Assert.True(textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out BraceSmartIndenter smartIndenter));
Assert.Same(expectedSmartIndenter, smartIndenter);
}
private static DefaultRazorEditorFactoryService CreateFactoryService(
VisualStudioDocumentTracker documentTracker = null,
VisualStudioRazorParser parser = null,
BraceSmartIndenter smartIndenter = null)
{
documentTracker = documentTracker ?? Mock.Of<VisualStudioDocumentTracker>();
parser = parser ?? Mock.Of<VisualStudioRazorParser>();
smartIndenter = smartIndenter ?? Mock.Of<BraceSmartIndenter>();
var documentTrackerFactory = Mock.Of<VisualStudioDocumentTrackerFactory>(f => f.Create(It.IsAny<ITextBuffer>()) == documentTracker);
var parserFactory = Mock.Of<VisualStudioRazorParserFactory>(f => f.Create(It.IsAny<VisualStudioDocumentTracker>()) == parser);
var smartIndenterFactory = Mock.Of<BraceSmartIndenterFactory>(f => f.Create(It.IsAny<VisualStudioDocumentTracker>()) == smartIndenter);
var factoryService = new DefaultRazorEditorFactoryService(documentTrackerFactory, parserFactory, smartIndenterFactory);
return factoryService;
}
}
}

View File

@ -1,297 +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.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultVisualStudioDocumentTrackerFactoryTest : ForegroundDispatcherTestBase
{
private static IReadOnlyList<ProjectSnapshot> Projects = new List<ProjectSnapshot>();
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(p => p.Projects == Projects);
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true);
private Workspace Workspace { get; } = new AdhocWorkspace();
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
[ForegroundFact]
public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
var tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[0], tracker.TextBuffer);
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView_ForMultipleBuffers()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
var tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[0], tracker.TextBuffer);
Assert.False(buffers[1].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
tracker = buffers[2].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
Assert.Equal(buffers[2], tracker.TextBuffer);
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_DoesNotAddDuplicateTextViewEntry()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Same(tracker, buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker)));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView));
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_AddsEntryForADifferentTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersConnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Same(tracker, buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker)));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1), v => Assert.Same(v, textView2));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[1]);
tracker.TextViewsInternal.Add(textView1);
tracker.TextViewsInternal.Add(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
factory.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
tracker = buffers[1].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[ForegroundFact]
public void GetTracker_ITextBuffer_ForRazorTextBufferWithTracker_ReturnsTracker()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection());
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, textBuffer);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
var result = factory.GetTracker(textBuffer);
// Assert
Assert.Same(tracker, result);
}
[ForegroundFact]
public void GetTracker_ITextBuffer_NonRazorBuffer_ReturnsNull()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var textBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection());
// Act
var result = factory.GetTracker(textBuffer);
// Assert
Assert.Null(result);
}
[ForegroundFact]
public void GetTracker_ITextView_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
var bufferGraph = Mock.Of<IBufferGraph>(g => g.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()) == buffers);
var textView = Mock.Of<IWpfTextView>(v => v.BufferGraph == bufferGraph);
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.TextViewsInternal.Add(textView);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
// Act
var result = factory.GetTracker(textView);
// Assert
Assert.Same(tracker, result);
}
[ForegroundFact]
public void GetTracker_ITextView_WithoutRazorBuffer_ReturnsNull()
{
// Arrange
var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace);
var buffers = new Collection<ITextBuffer>();
var bufferGraph = Mock.Of<IBufferGraph>(g => g.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()) == buffers);
var textView = Mock.Of<IWpfTextView>(v => v.BufferGraph == bufferGraph);
// Act
var result = factory.GetTracker(textView);
// Assert
Assert.Null(result);
}
}
}

View File

@ -0,0 +1,175 @@
// 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.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class DefaultVisualStudioDocumentTrackerTest
{
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private ITextBuffer TextBuffer => Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType);
private string FilePath => "C:/Some/Path/TestDocumentTracker.cshtml";
private ProjectSnapshotManager ProjectManager => Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private TextBufferProjectService ProjectService => Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private Workspace Workspace => new AdhocWorkspace();
[Fact]
public void AddTextView_AddsToTextViewCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
public void AddTextView_SubscribesAfterFirstTextViewAdded()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Assert - 1
Assert.False(documentTracker.IsSupportedProject);
// Act
documentTracker.AddTextView(textView);
// Assert - 2
Assert.True(documentTracker.IsSupportedProject);
}
[Fact]
public void AddTextView_DoesNotAddDuplicateTextViews()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView);
documentTracker.AddTextView(textView);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
public void AddTextView_AddsMultipleTextViewsToCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
// Assert
Assert.Collection(
documentTracker.TextViews,
v => Assert.Same(v, textView1),
v => Assert.Same(v, textView2));
}
[Fact]
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
documentTracker.AddTextView(textView);
// Act
documentTracker.RemoveTextView(textView);
// Assert
Assert.Empty(documentTracker.TextViews);
}
[Fact]
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var textView3 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
documentTracker.AddTextView(textView3);
// Act
documentTracker.RemoveTextView(textView2);
// Assert
Assert.Collection(
documentTracker.TextViews,
v => Assert.Same(v, textView1),
v => Assert.Same(v, textView3));
}
[Fact]
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
var textView2 = Mock.Of<ITextView>();
// Act
documentTracker.RemoveTextView(textView2);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1));
}
[Fact]
public void RemoveTextView_UnsubscribesAfterLastTextViewRemoved()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
// Act - 1
documentTracker.RemoveTextView(textView1);
// Assert - 1
Assert.True(documentTracker.IsSupportedProject);
// Act - 2
documentTracker.RemoveTextView(textView2);
// Assert - 2
Assert.False(documentTracker.IsSupportedProject);
}
}
}

View File

@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
// Arrange
var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World");
var parser = new VisualStudioRazorParser(expectedCodeDocument);
var parser = new DefaultVisualStudioRazorParser(expectedCodeDocument);
var properties = new PropertyCollection();
properties.AddProperty(typeof(VisualStudioRazorParser), parser);
var textBuffer = new Mock<ITextBuffer>();
@ -60,7 +60,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
// Arrange
var properties = new PropertyCollection();
var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World");
var parser = new VisualStudioRazorParser(expectedCodeDocument);
var parser = new DefaultVisualStudioRazorParser(expectedCodeDocument);
properties.AddProperty(typeof(VisualStudioRazorParser), parser);
var unexpectedCodeDocument = TestRazorCodeDocument.Create("Unexpected");
var legacyParser = new RazorEditorParser(unexpectedCodeDocument);

View File

@ -0,0 +1,126 @@
// 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.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class RazorTextViewConnectionListenerTest : ForegroundDispatcherTestBase
{
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private Workspace Workspace { get; } = new AdhocWorkspace();
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
[ForegroundFact]
public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var editorFactoryService = new Mock<RazorEditorFactoryService>(MockBehavior.Strict);
var factory = new RazorTextViewConnectionListener(Dispatcher, editorFactoryService.Object, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Act & Assert
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBuffer_AddsTextViewToTracker()
{
// Arrange
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
VisualStudioDocumentTracker documentTracker = new DefaultVisualStudioDocumentTracker("AFile", ProjectManager, ProjectService, Workspace, buffers[0]);
var editorFactoryService = Mock.Of<RazorEditorFactoryService>(factoryService => factoryService.TryGetDocumentTracker(It.IsAny<ITextBuffer>(), out documentTracker) == true);
var factory = new RazorTextViewConnectionListener(Dispatcher, editorFactoryService, Workspace);
// Act
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView()
{
// Arrange
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, Workspace, buffers[0]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, Workspace, buffers[1]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
var factory = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
// Act
factory.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
tracker = buffers[1].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var factory = new RazorTextViewConnectionListener(Dispatcher, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
factory.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
}
}

View File

@ -4,6 +4,8 @@
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using Microsoft.VisualStudio.ComponentModelHost;
@ -11,6 +13,7 @@ using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
@ -20,15 +23,15 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
internal class RazorDocumentInfoWindow : ToolWindowPane
{
private IVsEditorAdaptersFactoryService _adapterFactory;
private VisualStudioDocumentTrackerFactory _documentTrackerService;
private RazorEditorFactoryService _editorFactoryService;
private IVsTextManager _textManager;
private IVsRunningDocumentTable _rdt;
private uint _cookie;
private ITextView _textView;
private VisualStudioDocumentTracker _documentTracker;
public RazorDocumentInfoWindow()
public RazorDocumentInfoWindow()
: base(null)
{
Caption = "Razor Document Info";
@ -42,11 +45,11 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
var component = (IComponentModel)GetService(typeof(SComponentModel));
_adapterFactory = component.GetService<IVsEditorAdaptersFactoryService>();
_documentTrackerService = component.GetService<VisualStudioDocumentTrackerFactory>();
_editorFactoryService = component.GetService<RazorEditorFactoryService>();
_textManager = (IVsTextManager)GetService(typeof(SVsTextManager));
_rdt = (IVsRunningDocumentTable)GetService(typeof(SVsRunningDocumentTable));
var hr = _rdt.AdviseRunningDocTableEvents(new RdtEvents(this), out uint _cookie);
ErrorHandler.ThrowOnFailure(hr);
}
@ -77,7 +80,13 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
_documentTracker.ContextChanged -= DocumentTracker_ContextChanged;
}
_documentTracker = _documentTrackerService.GetTracker(textView);
var textBuffer = textView.BufferGraph.GetRazorBuffers().FirstOrDefault();
if (!_editorFactoryService.TryGetDocumentTracker(textBuffer, out _documentTracker))
{
return;
}
_documentTracker.ContextChanged += DocumentTracker_ContextChanged;
((FrameworkElement)Content).DataContext = new RazorDocumentInfoViewModel(_documentTracker);