aspnetcore/src/Microsoft.VisualStudio.Edit.../DefaultVisualStudioRazorPar...

404 lines
13 KiB
C#

// 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.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
using Timer = System.Threading.Timer;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class DefaultVisualStudioRazorParser : VisualStudioRazorParser, IDisposable
{
public override event EventHandler<DocumentStructureChangedEventArgs> DocumentStructureChanged;
// Internal for testing.
internal TimeSpan IdleDelay = TimeSpan.FromSeconds(3);
internal Timer _idleTimer;
internal BackgroundParser _parser;
private readonly object IdleLock = new object();
private readonly ICompletionBroker _completionBroker;
private readonly 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)
{
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));
}
_dispatcher = dispatcher;
_templateEngineFactory = templateEngineFactory;
_errorReporter = errorReporter;
_completionBroker = completionBroker;
_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 void QueueReparse()
{
// Can be called from any thread
if (_dispatcher.IsForegroundThread)
{
ReparseOnForeground(null);
}
else
{
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;
}
// 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.
QueueReparse();
}
// 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 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;
}
}
QueueReparse();
}
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(_documentTracker.EditorSettings));
builder.Features.Add(new VisualStudioTagHelperFeature(TextBuffer));
}
private class VisualStudioParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
{
private readonly EditorSettings _settings;
public VisualStudioParserOptionsFeature(EditorSettings settings)
{
_settings = settings;
}
public int Order { get; set; }
public void Configure(RazorCodeGenerationOptionsBuilder options)
{
options.IndentSize = _settings.IndentSize;
options.IndentWithTabs = _settings.IndentWithTabs;
}
}
/// <summary>
/// This class will cease to be useful once we control TagHelper discovery. For now, it delegates discovery
/// to ITagHelperFeature's that exist on the text buffer.
/// </summary>
private class VisualStudioTagHelperFeature : ITagHelperFeature
{
private readonly ITextBuffer _textBuffer;
public VisualStudioTagHelperFeature(ITextBuffer textBuffer)
{
_textBuffer = textBuffer;
}
public RazorEngine Engine { get; set; }
public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
{
if (_textBuffer.Properties.TryGetProperty(typeof(ITagHelperFeature), out ITagHelperFeature feature))
{
return feature.GetDescriptors();
}
return Array.Empty<TagHelperDescriptor>();
}
}
}
}