Skip to content

Commit

Permalink
Avoid rooting documents for languages that support syntax trees
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Apr 25, 2023
1 parent ef625f3 commit 7418438
Showing 1 changed file with 42 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;
Expand Down Expand Up @@ -69,7 +71,7 @@ internal partial class TagComputer
// get called.

private readonly object _gate = new();
private (ITextSnapshot? lastSnapshot, Document? lastDocument, SyntaxNode? lastRoot) _lastProcessedData;
private (ITextSnapshot lastSnapshot, SumType<SyntaxNode, Document> lastDocumentOrRoot)? _lastProcessedData;

// this will cache previous classification information for a span, so that we can avoid
// digging into same tree again and again to find exactly same answer
Expand Down Expand Up @@ -109,8 +111,16 @@ public TagComputer(

public event EventHandler<SnapshotSpanEventArgs>? TagsChanged;

private IClassificationService? TryGetClassificationService(ITextSnapshot snapshot)
=> _workspace?.Services.SolutionServices.GetProjectServices(snapshot.ContentType)?.GetService<IClassificationService>();
private (SolutionServices solutionServices, IClassificationService classificationService)? TryGetClassificationService(ITextSnapshot snapshot)
{
if (_workspace?.Services.SolutionServices is not { } solutionServices)
return null;

if (solutionServices.GetProjectServices(snapshot.ContentType)?.GetService<IClassificationService>() is not { } classificationService)
return null;

return (solutionServices, classificationService);
}

#region Workspace Hookup

Expand Down Expand Up @@ -193,7 +203,7 @@ public void DisconnectFromWorkspace()

lock (_gate)
{
_lastProcessedData = default;
_lastProcessedData = null;
}

if (_workspace != null)
Expand Down Expand Up @@ -272,26 +282,29 @@ private async ValueTask ProcessChangesAsync(ImmutableSegmentedList<ITextSnapshot
Contract.ThrowIfTrue(snapshots.IsDefault || snapshots.IsEmpty);
var currentSnapshot = GetLatest(snapshots);

var classificationService = TryGetClassificationService(currentSnapshot);
if (classificationService == null)
if (TryGetClassificationService(currentSnapshot) is not (var solutionServices, var classificationService))
return;

var currentDocument = currentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (currentDocument == null)
return;

var (previousSnapshot, previousDocument, previousRoot) = GetLastProcessedData();
SumType<SyntaxNode, Document>? previousDocumentOrRoot = null;
if (GetLastProcessedData() is { } data)
{
previousDocumentOrRoot = data.lastDocumentOrRoot;
}

// Optionally pre-calculate the root of the doc so that it is ready to classify
// once GetTags is called. Also, attempt to determine a smaller change range span
// for this document so that we can avoid reporting the entire document as changed.

var currentRoot = await currentDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var changedSpan = await ComputeChangedSpanAsync().ConfigureAwait(false);
var changedSpan = await ComputeChangedSpanAsync(solutionServices, classificationService, previousDocumentOrRoot, currentSnapshot, currentDocument, currentRoot, _diffTimeout, cancellationToken).ConfigureAwait(false);

lock (_gate)
{
_lastProcessedData = (currentSnapshot, currentDocument, currentRoot);
_lastProcessedData = (currentSnapshot, currentRoot is not null ? currentRoot : currentDocument);
}

// Notify the editor now that there were changes. Note: we do not need to go the
Expand All @@ -313,29 +326,29 @@ static ITextSnapshot GetLatest(ImmutableSegmentedList<ITextSnapshot> snapshots)
return latest;
}

async ValueTask<SnapshotSpan> ComputeChangedSpanAsync()
static async ValueTask<SnapshotSpan> ComputeChangedSpanAsync(SolutionServices solutionServices, IClassificationService classificationService, SumType<SyntaxNode, Document>? previousDocumentOrRoot, ITextSnapshot currentSnapshot, Document currentDocument, SyntaxNode? currentRoot, TimeSpan diffTimeout, CancellationToken cancellationToken)
{
var changeRange = await ComputeChangedRangeAsync().ConfigureAwait(false);
var changeRange = await ComputeChangedRangeAsync(solutionServices, classificationService, previousDocumentOrRoot, currentDocument, currentRoot, diffTimeout, cancellationToken).ConfigureAwait(false);
return changeRange != null
? currentSnapshot.GetSpan(changeRange.Value.Span.Start, changeRange.Value.NewLength)
: currentSnapshot.GetFullSpan();
}

ValueTask<TextChangeRange?> ComputeChangedRangeAsync()
static ValueTask<TextChangeRange?> ComputeChangedRangeAsync(SolutionServices solutionServices, IClassificationService classificationService, SumType<SyntaxNode, Document>? previousDocumentOrRoot, Document currentDocument, SyntaxNode? currentRoot, TimeSpan diffTimeout, CancellationToken cancellationToken)
{
// If we have syntax available fast path the change computation without async or blocking.
if (previousRoot != null && currentRoot != null)
return new(classificationService.ComputeSyntacticChangeRange(currentDocument.Project.Solution.Services, previousRoot, currentRoot, _diffTimeout, cancellationToken));
if ((previousDocumentOrRoot?.TryGetFirst(out var previousRoot) ?? false) && currentRoot != null)
return new(classificationService.ComputeSyntacticChangeRange(solutionServices, previousRoot, currentRoot, diffTimeout, cancellationToken));

// Otherwise, fall back to the language to compute the difference based on the document contents.
if (previousDocument != null)
return classificationService.ComputeSyntacticChangeRangeAsync(previousDocument, currentDocument, _diffTimeout, cancellationToken);
if (previousDocumentOrRoot?.TryGetSecond(out var previousDocument) ?? false)
return classificationService.ComputeSyntacticChangeRangeAsync(previousDocument, currentDocument, diffTimeout, cancellationToken);

return new ValueTask<TextChangeRange?>();
}
}

private (ITextSnapshot? lastSnapshot, Document? lastDocument, SyntaxNode? lastRoot) GetLastProcessedData()
private (ITextSnapshot lastSnapshot, SumType<SyntaxNode, Document> lastDocumentOrRoot)? GetLastProcessedData()
{
lock (_gate)
return _lastProcessedData;
Expand All @@ -359,8 +372,7 @@ public IEnumerable<ITagSpan<IClassificationTag>> GetTags(NormalizedSnapshotSpanC

var snapshot = spans[0].Snapshot;

var classificationService = TryGetClassificationService(snapshot);
if (classificationService == null)
if (TryGetClassificationService(snapshot) is not (var solutionServices, var classificationService))
return null;

using var _ = Classifier.GetPooledList(out var classifiedSpans);
Expand All @@ -375,31 +387,26 @@ void AddClassifications(SnapshotSpan span)
_taggerProvider._threadingContext.ThrowIfNotOnUIThread();

// First, get the tree and snapshot that we'll be operating over.
var (lastProcessedSnapshot, lastProcessedDocument, lastProcessedRoot) = GetLastProcessedData();

if (lastProcessedDocument == null)
if (GetLastProcessedData() is not (var lastProcessedSnapshot, var lastProcessedDocumentOrRoot))
{
// We don't have a syntax tree yet. Just do a lexical classification of the document.
AddLexicalClassifications(classificationService, span, classifiedSpans);
return;
}

// If we have a document, we must have a snapshot as well.
Contract.ThrowIfNull(lastProcessedSnapshot);

// We have a tree. However, the tree may be for an older version of the snapshot.
// If it is for an older version, then classify that older version and translate
// the classifications forward. Otherwise, just classify normally.

if (lastProcessedSnapshot.Version.ReiteratedVersionNumber != span.Snapshot.Version.ReiteratedVersionNumber)
{
// Slightly more complicated. We have a parse tree, it's just not for the snapshot we're being asked for.
AddClassifiedSpansForPreviousDocument(classificationService, span, lastProcessedSnapshot, lastProcessedDocument, lastProcessedRoot, classifiedSpans);
AddClassifiedSpansForPreviousDocument(solutionServices, classificationService, span, lastProcessedSnapshot, lastProcessedDocumentOrRoot, classifiedSpans);
return;
}

// Mainline case. We have the corresponding document for the snapshot we're classifying.
AddSyntacticClassificationsForDocument(classificationService, span, lastProcessedDocument, lastProcessedRoot, classifiedSpans);
AddSyntacticClassificationsForDocument(solutionServices, classificationService, span, lastProcessedDocumentOrRoot, classifiedSpans);
}
}

Expand All @@ -412,8 +419,8 @@ private void AddLexicalClassifications(IClassificationService classificationServ
}

private void AddSyntacticClassificationsForDocument(
IClassificationService classificationService, SnapshotSpan span,
Document document, SyntaxNode? root, SegmentedList<ClassifiedSpan> classifiedSpans)
SolutionServices solutionServices, IClassificationService classificationService, SnapshotSpan span,
SumType<SyntaxNode, Document> lastProcessedDocumentOrRoot, SegmentedList<ClassifiedSpan> classifiedSpans)
{
_taggerProvider._threadingContext.ThrowIfNotOnUIThread();
var cancellationToken = CancellationToken.None;
Expand All @@ -424,18 +431,18 @@ private void AddSyntacticClassificationsForDocument(
using var _ = Classifier.GetPooledList(out var tempList);

// If we have a syntax root ready, use the direct, non-async/non-blocking approach to getting classifications.
if (root == null)
classificationService.AddSyntacticClassificationsAsync(document, span.Span.ToTextSpan(), tempList, cancellationToken).Wait(cancellationToken);
if (!lastProcessedDocumentOrRoot.TryGetFirst(out var root))
classificationService.AddSyntacticClassificationsAsync(lastProcessedDocumentOrRoot.Second, span.Span.ToTextSpan(), tempList, cancellationToken).Wait(cancellationToken);
else
classificationService.AddSyntacticClassifications(document.Project.Solution.Services, root, span.Span.ToTextSpan(), tempList, cancellationToken);
classificationService.AddSyntacticClassifications(solutionServices, root, span.Span.ToTextSpan(), tempList, cancellationToken);

_lastLineCache.Update(span, tempList);
classifiedSpans.AddRange(tempList);
}

private void AddClassifiedSpansForPreviousDocument(
IClassificationService classificationService, SnapshotSpan span,
ITextSnapshot lastProcessedSnapshot, Document lastProcessedDocument, SyntaxNode? lastProcessedRoot,
SolutionServices solutionServices, IClassificationService classificationService, SnapshotSpan span,
ITextSnapshot lastProcessedSnapshot, SumType<SyntaxNode, Document> lastProcessedDocumentOrRoot,
SegmentedList<ClassifiedSpan> classifiedSpans)
{
_taggerProvider._threadingContext.ThrowIfNotOnUIThread();
Expand All @@ -461,7 +468,7 @@ private void AddClassifiedSpansForPreviousDocument(
{
using var _ = Classifier.GetPooledList(out var tempList);

AddSyntacticClassificationsForDocument(classificationService, translatedSpan, lastProcessedDocument, lastProcessedRoot, tempList);
AddSyntacticClassificationsForDocument(solutionServices, classificationService, translatedSpan, lastProcessedDocumentOrRoot, tempList);

var currentSnapshot = span.Snapshot;
var currentText = currentSnapshot.AsText();
Expand Down

0 comments on commit 7418438

Please sign in to comment.