diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs index 635f3d18e6..ff9c69c4e2 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs @@ -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; } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenterFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenterFactory.cs new file mode 100644 index 0000000000..750c061b7b --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenterFactory.cs @@ -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); + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTextBufferCodeDocumentProvider.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTextBufferCodeDocumentProvider.cs index 5ee3552688..3298c414d0 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTextBufferCodeDocumentProvider.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTextBufferCodeDocumentProvider.cs @@ -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; diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs new file mode 100644 index 0000000000..9e167d909f --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -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 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 _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 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)); + } + + /// + /// This class will cease to be useful once we harvest/monitor settings from the editor. + /// + private class VisualStudioParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature + { + public int Order { get; set; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) + { + options.IndentSize = 4; + options.IndentWithTabs = false; + } + } + + /// + /// 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. + /// + private class VisualStudioTagHelperFeature : ITagHelperFeature + { + private readonly ITextBuffer _textBuffer; + + public VisualStudioTagHelperFeature(ITextBuffer textBuffer) + { + _textBuffer = textBuffer; + } + + public RazorEngine Engine { get; set; } + + public IReadOnlyList GetDescriptors() + { + if (_textBuffer.Properties.TryGetProperty(typeof(ITagHelperFeature), out ITagHelperFeature feature)) + { + return feature.GetDescriptors(); + } + + return Array.Empty(); + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DocumentStructureChangedEventArgs.cs b/src/Microsoft.VisualStudio.Editor.Razor/DocumentStructureChangedEventArgs.cs index 0aca0774f7..4cd5e5c7dc 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DocumentStructureChangedEventArgs.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DocumentStructureChangedEventArgs.cs @@ -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, diff --git a/src/Microsoft.VisualStudio.Editor.Razor/IContextChangedListener.cs b/src/Microsoft.VisualStudio.Editor.Razor/IContextChangedListener.cs new file mode 100644 index 0000000000..7b044dec8b --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/IContextChangedListener.cs @@ -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 +{ + /// + /// This class will cease to be useful once the Razor tooling owns TagHelper discovery + /// + public interface IContextChangedListener + { + void OnContextChanged(VisualStudioRazorParser parser); + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/RazorEditorFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/RazorEditorFactoryService.cs new file mode 100644 index 0000000000..5e4d18dac5 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/RazorEditorFactoryService.cs @@ -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); + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs index 2c4c6858d6..9b284aa8b0 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs @@ -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; } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs index 7795f83ddc..1576ac9074 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTrackerFactory.cs @@ -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); } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs index c0e75084f7..8d4a4fc609 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParser.cs @@ -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 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 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(); } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParserFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParserFactory.cs new file mode 100644 index 0000000000..580b33cc62 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioRazorParserFactory.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultBraceSmartIndenterFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultBraceSmartIndenterFactory.cs new file mode 100644 index 0000000000..9b2bcf0ad0 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultBraceSmartIndenterFactory.cs @@ -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(); + } + + 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; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultRazorEditorFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultRazorEditorFactoryService.cs new file mode 100644 index 0000000000..f555652aa9 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultRazorEditorFactoryService.cs @@ -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); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs index a83d7e769c..21f2298dcf 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs @@ -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 _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 TextViews => _textViews; - public IList 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(); - - 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() - .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) { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs index 2ed1a1ad23..7ec2d97b41 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs @@ -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(); _projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); } - // 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 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 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(); - } - } - } - } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioRazorParserFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioRazorParserFactory.cs new file mode 100644 index 0000000000..65c6342d04 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioRazorParserFactory.cs @@ -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 _parserContextChangedListeners; + private readonly ErrorReporter _errorReporter; + + [ImportingConstructor] + public DefaultVisualStudioRazorParserFactory( + ICompletionBroker completionBroker, + [ImportMany(typeof(IContextChangedListener))] IEnumerable 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(); + _errorReporter = workspace.Services.GetRequiredService(); + var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name); + _templateEngineFactoryService = razorLanguageServices.GetRequiredService(); + } + + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextViewConnectionListener.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextViewConnectionListener.cs new file mode 100644 index 0000000000..343e6be021 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextViewConnectionListener.cs @@ -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(); + } + + // 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 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 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); + } + } + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs index 0287895386..cf7337b86f 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterIntegrationTest.cs @@ -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); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs index 5fa523db88..9dcab10341 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs @@ -65,9 +65,10 @@ namespace Microsoft.VisualStudio.Editor.Razor var editorOperations = new Mock(); editorOperations.Setup(operations => operations.MoveToEndOfLine(false)); var editorOperationsFactory = new Mock(); + var documentTracker = CreateDocumentTracker(() => Mock.Of(), textView); editorOperationsFactory.Setup(factory => factory.GetEditorOperations(textView)) .Returns(editorOperations.Object); - var smartIndenter = new BraceSmartIndenter(Dispatcher, new Mock().Object, new Mock().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(); var editorOperationsFactory = new Mock(); var changeCollection = new TestTextChangeCollection(); var textContentChangeArgs = new TestTextContentChangedEventArgs(changeCollection); - var documentTrackerFactory = new Mock(); - var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer.Object, documentTrackerFactory.Object, editorOperationsFactory.Object); + var documentTracker = CreateDocumentTracker(() => Mock.Of(), Mock.Of()); + 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(); - var documentTrackerFactory = new Mock(); - var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, textBuffer, documentTrackerFactory.Object, editorOperationsFactory.Object); + var documentTracker = CreateDocumentTracker(() => textBuffer, Mock.Of()); + var braceSmartIndenter = new BraceSmartIndenter(Dispatcher, documentTracker, editorOperationsFactory.Object); // Act & Assert textBuffer.ApplyEdits(edit, edit); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs index 747f8d8713..679d2674c5 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTestBase.cs @@ -24,15 +24,6 @@ namespace Microsoft.VisualStudio.Editor.Razor return tracker.Object; } - protected static VisualStudioDocumentTrackerFactory CreateDocumentTrackerFactory(Func bufferAccessor, VisualStudioDocumentTracker documentTracker) - { - var trackerFactory = new Mock(); - trackerFactory.Setup(factory => factory.GetTracker(It.IsAny())) - .Returns(documentTracker); - - return trackerFactory.Object; - } - protected static ITextView CreateFocusedTextView(Func textBufferAccessor = null, ITextCaret caret = null) { var focusedTextView = new Mock(); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs index 79a725927d..863b02110f 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTemplateEngineFactoryServiceTest.cs @@ -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(), - Mock.Of(), - Mock.Of(), - Enumerable.Empty(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Enumerable.Empty(), workspace) { } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTextBufferCodeDocumentProviderTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTextBufferCodeDocumentProviderTest.cs index 12538735de..12ebfd2330 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTextBufferCodeDocumentProviderTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTextBufferCodeDocumentProviderTest.cs @@ -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(); - textBuffer.Setup(buffer => buffer.Properties) - .Returns(properties); - var provider = new DefaultTextBufferCodeDocumentProvider(); + VisualStudioRazorParser parser = new DefaultVisualStudioRazorParser(expectedCodeDocument); + var parserProvider = Mock.Of(p => p.TryGetParser(It.IsAny(), out parser) == true); + var textBuffer = Mock.Of(); + 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(p => p.TryGetParser(It.IsAny(), out parser) == true); + var textBuffer = Mock.Of(); + 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(); - textBuffer.Setup(buffer => buffer.Properties) - .Returns(properties); - var provider = new DefaultTextBufferCodeDocumentProvider(); + VisualStudioRazorParser parser = null; + var parserProvider = Mock.Of(p => p.TryGetParser(It.IsAny(), out parser) == false); + var textBuffer = Mock.Of(); + 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); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs similarity index 76% rename from test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs rename to test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs index 10b3a29000..2e31a0f90f 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/VisualStudioRazorParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs @@ -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("filePath", - () => new VisualStudioRazorParser( - Dispatcher, - new TestTextBuffer(null), - CreateTemplateEngine(), - null, - new DefaultErrorReporter(), - new TestCompletionBroker(), - new Mock().Object, - new Mock().Object)); - } - - [Fact] - public void ConstructorRequiresNonEmptyPhysicalPath() - { - Assert.Throws("filePath", - () => new VisualStudioRazorParser( - Dispatcher, - new TestTextBuffer(null), - CreateTemplateEngine(), - string.Empty, - new DefaultErrorReporter(), - new TestCompletionBroker(), - new Mock().Object, - new Mock().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().Object, - new Mock().Object)) + Enumerable.Empty())) { - 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().Object, - new Mock().Object); + new TestCompletionBroker(), + Enumerable.Empty()) + { + // 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 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( + service => service.Create(It.IsAny(), It.IsAny>()) == 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(textView => textView.HasAggregateFocus == true); + var documentTracker = Mock.Of(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() diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs new file mode 100644 index 0000000000..9cc348f08f --- /dev/null +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs @@ -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(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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty()) + { + 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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty()) + { + 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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty())) + { + 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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty())) + { + // Act + parser.StartParser(); + + // Assert + Assert.Equal(1, textBuffer.AttachedChangedEvents.Count); + Assert.NotNull(parser._parser); + } + } + + [ForegroundFact] + public void NotifyParserContextChanged_NotifiesListeners() + { + // Arrange + var listener1 = new Mock(); + listener1.Setup(l => l.OnContextChanged(It.IsAny())); + var listener2 = new Mock(); + listener2.Setup(l => l.OnContextChanged(It.IsAny())); + using (var parser = new DefaultVisualStudioRazorParser( + Dispatcher, + CreateDocumentTracker(), + Mock.Of(), + new DefaultErrorReporter(), + Mock.Of(), + 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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty())) + { + // 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(), + new DefaultErrorReporter(), + Mock.Of(), + Enumerable.Empty())) + { + // Act + var result = parser.TryReinitializeParser(); + + // Assert + Assert.False(result); + } + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs index 5f7a658b8c..cde8d8dc36 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Infrastructure/TestTextBuffer.cs @@ -11,10 +11,13 @@ namespace Microsoft.VisualStudio.Test public class TestTextBuffer : ITextBuffer { private ITextSnapshot _currentSnapshot; + private List> _attachedChangedEvents; public TestTextBuffer(ITextSnapshot initialSnapshot) { _currentSnapshot = initialSnapshot; + _attachedChangedEvents = new List>(); + 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> AttachedChangedEvents => _attachedChangedEvents; + public ITextSnapshot CurrentSnapshot => _currentSnapshot; public PropertyCollection Properties { get; } public event EventHandler ReadOnlyRegionsChanged; - public event EventHandler Changed; + + public event EventHandler Changed + { + add + { + _attachedChangedEvents.Add(value); + } + remove + { + _attachedChangedEvents.Remove(value); + } + } + + public event EventHandler ChangedLowPriority; public event EventHandler ChangedHighPriority; public event EventHandler Changing; diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultRazorEditorFactoryServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultRazorEditorFactoryServiceTest.cs new file mode 100644 index 0000000000..f92620cec7 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultRazorEditorFactoryServiceTest.cs @@ -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(c => c.IsOfType(RazorLanguage.ContentType) == true); + + private IContentType NonRazorContentType { get; } = Mock.Of(c => c.IsOfType(It.IsAny()) == false); + + [Fact] + public void TryGetDocumentTracker_ForRazorTextBuffer_ReturnsTrue() + { + // Arrange + var expectedDocumentTracker = Mock.Of(); + var factoryService = CreateFactoryService(expectedDocumentTracker); + var textBuffer = Mock.Of(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(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(); + var factoryService = CreateFactoryService(expectedDocumentTracker); + var textBuffer = Mock.Of(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(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(); + var factoryService = CreateFactoryService(parser: expectedParser); + var textBuffer = Mock.Of(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(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(); + var factoryService = CreateFactoryService(parser: expectedParser); + var textBuffer = Mock.Of(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(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(); + var factoryService = CreateFactoryService(smartIndenter: expectedSmartIndenter); + var textBuffer = Mock.Of(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(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(); + var factoryService = CreateFactoryService(smartIndenter: expectedSmartIndenter); + var textBuffer = Mock.Of(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(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(); + parser = parser ?? Mock.Of(); + smartIndenter = smartIndenter ?? Mock.Of(); + + var documentTrackerFactory = Mock.Of(f => f.Create(It.IsAny()) == documentTracker); + var parserFactory = Mock.Of(f => f.Create(It.IsAny()) == parser); + var smartIndenterFactory = Mock.Of(f => f.Create(It.IsAny()) == smartIndenter); + var factoryService = new DefaultRazorEditorFactoryService(documentTrackerFactory, parserFactory, smartIndenterFactory); + + return factoryService; + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs deleted file mode 100644 index 5d9759c410..0000000000 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs +++ /dev/null @@ -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 Projects = new List(); - - private ProjectSnapshotManager ProjectManager { get; } = Mock.Of(p => p.Projects == Projects); - - private TextBufferProjectService ProjectService { get; } = Mock.Of( - s => s.GetHierarchy(It.IsAny()) == Mock.Of() && - s.IsSupportedProject(It.IsAny()) == true); - - private Workspace Workspace { get; } = new AdhocWorkspace(); - - private IContentType RazorContentType { get; } = Mock.Of(c => c.IsOfType(RazorLanguage.ContentType) == true); - - private IContentType NonRazorContentType { get; } = Mock.Of(c => c.IsOfType(It.IsAny()) == false); - - [ForegroundFact] - public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing() - { - // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace); - - var textView = Mock.Of(); - - var buffers = new Collection() - { - Mock.Of(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(); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - }; - - // Act - factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers); - - // Assert - var tracker = buffers[0].Properties.GetProperty(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(); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - Mock.Of(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()), - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - }; - - // Act - factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers); - - // Assert - var tracker = buffers[0].Properties.GetProperty(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(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(); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - }; - - // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, 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(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(); - var textView2 = Mock.Of(); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - }; - - // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, 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(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(); - var textView2 = Mock.Of(); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - Mock.Of(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(typeof(VisualStudioDocumentTracker)); - Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1)); - - tracker = buffers[1].Properties.GetProperty(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(); - - var buffers = new Collection() - { - Mock.Of(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(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()); - - // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, textBuffer); - textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); - - // Act - var result = factory.GetTracker(textBuffer); - - // Assert - Assert.Same(tracker, result); - } - - [ForegroundFact] - public void GetTracker_ITextBuffer_NonRazorBuffer_ReturnsNull() - { - // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace); - var textBuffer = Mock.Of(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()); - - // Act - var result = factory.GetTracker(textBuffer); - - // Assert - Assert.Null(result); - } - - [ForegroundFact] - public void GetTracker_ITextView_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker() - { - // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(Dispatcher, ProjectManager, ProjectService, Workspace); - - var buffers = new Collection() - { - Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), - }; - - var bufferGraph = Mock.Of(g => g.GetTextBuffers(It.IsAny>()) == buffers); - - var textView = Mock.Of(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(); - - var bufferGraph = Mock.Of(g => g.GetTextBuffers(It.IsAny>()) == buffers); - - var textView = Mock.Of(v => v.BufferGraph == bufferGraph); - - // Act - var result = factory.GetTracker(textView); - - // Assert - Assert.Null(result); - } - } -} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerTest.cs new file mode 100644 index 0000000000..aa7f5c0f92 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerTest.cs @@ -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(c => c.IsOfType(RazorLanguage.ContentType) == true); + + private ITextBuffer TextBuffer => Mock.Of(b => b.ContentType == RazorContentType); + + private string FilePath => "C:/Some/Path/TestDocumentTracker.cshtml"; + + private ProjectSnapshotManager ProjectManager => Mock.Of(p => p.Projects == new List()); + + private TextBufferProjectService ProjectService => Mock.Of( + s => s.GetHierarchy(It.IsAny()) == Mock.Of() && + s.IsSupportedProject(It.IsAny()) == true && + s.GetProjectPath(It.IsAny()) == "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(); + + // 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(); + + // 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(); + + // 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(); + var textView2 = Mock.Of(); + + // 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(); + 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(); + var textView2 = Mock.Of(); + var textView3 = Mock.Of(); + 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(); + documentTracker.AddTextView(textView1); + var textView2 = Mock.Of(); + + // 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(); + var textView2 = Mock.Of(); + 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); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/LegacyTextBufferCodeDocumentProviderTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/LegacyTextBufferCodeDocumentProviderTest.cs index f960a84ffe..4127851928 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/LegacyTextBufferCodeDocumentProviderTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/LegacyTextBufferCodeDocumentProviderTest.cs @@ -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(); @@ -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); diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorTextViewConnectionListenerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorTextViewConnectionListenerTest.cs new file mode 100644 index 0000000000..a9c6f34f92 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorTextViewConnectionListenerTest.cs @@ -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(p => p.Projects == new List()); + + private TextBufferProjectService ProjectService { get; } = Mock.Of( + s => s.GetHierarchy(It.IsAny()) == Mock.Of() && + s.IsSupportedProject(It.IsAny()) == true && + s.GetProjectPath(It.IsAny()) == "C:/Some/Path/TestProject.csproj"); + + private Workspace Workspace { get; } = new AdhocWorkspace(); + + private IContentType RazorContentType { get; } = Mock.Of(c => c.IsOfType(RazorLanguage.ContentType) == true); + + private IContentType NonRazorContentType { get; } = Mock.Of(c => c.IsOfType(It.IsAny()) == false); + + [ForegroundFact] + public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing() + { + // Arrange + var editorFactoryService = new Mock(MockBehavior.Strict); + var factory = new RazorTextViewConnectionListener(Dispatcher, editorFactoryService.Object, Workspace); + var textView = Mock.Of(); + var buffers = new Collection() + { + Mock.Of(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(); + var buffers = new Collection() + { + Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), + }; + VisualStudioDocumentTracker documentTracker = new DefaultVisualStudioDocumentTracker("AFile", ProjectManager, ProjectService, Workspace, buffers[0]); + var editorFactoryService = Mock.Of(factoryService => factoryService.TryGetDocumentTracker(It.IsAny(), 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(); + var textView2 = Mock.Of(); + + var buffers = new Collection() + { + Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), + Mock.Of(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(), Workspace); + + // Act + factory.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers); + + // Assert + tracker = buffers[0].Properties.GetProperty(typeof(VisualStudioDocumentTracker)); + Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1)); + + tracker = buffers[1].Properties.GetProperty(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(), Workspace); + + var textView = Mock.Of(); + + var buffers = new Collection() + { + Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), + }; + + // Act + factory.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers); + + // Assert + Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker))); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindow.cs b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindow.cs index 313f068573..ae2a02e6d1 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindow.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindow.cs @@ -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(); - _documentTrackerService = component.GetService(); - + _editorFactoryService = component.GetService(); + _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);