aspnetcore/src/Microsoft.VisualStudio.Edit.../VisualStudioRazorParser.cs

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