Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure taggers that compute frozen partial data eventually move to a 'correct' final state. #72878

Merged
merged 36 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
db6b65e
Initial work to support frozen partial tagging
CyrusNajmabadi Apr 4, 2024
0dab34b
Update tests
CyrusNajmabadi Apr 4, 2024
209a95c
Add tests
CyrusNajmabadi Apr 4, 2024
1411434
Update tests
CyrusNajmabadi Apr 4, 2024
be9305d
in progress
CyrusNajmabadi Apr 4, 2024
303982a
Cancellation series
CyrusNajmabadi Apr 4, 2024
06116b2
comments
CyrusNajmabadi Apr 4, 2024
b976155
move docs
CyrusNajmabadi Apr 4, 2024
4e3d729
Simplify
CyrusNajmabadi Apr 4, 2024
d869168
ASserts
CyrusNajmabadi Apr 4, 2024
7c8bf35
Update src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProv…
CyrusNajmabadi Apr 4, 2024
3ec3530
Simplify
CyrusNajmabadi Apr 4, 2024
f554737
Merge branch 'frozenPartialTagging' of https://github.com/CyrusNajmab…
CyrusNajmabadi Apr 4, 2024
24c34e5
move helpers
CyrusNajmabadi Apr 4, 2024
3de1764
indentatin
CyrusNajmabadi Apr 4, 2024
84c7aab
REmove type
CyrusNajmabadi Apr 4, 2024
5469dee
Docs
CyrusNajmabadi Apr 4, 2024
766085b
REmove unused usings
CyrusNajmabadi Apr 4, 2024
d9c8454
Consistentcy
CyrusNajmabadi Apr 4, 2024
02bd1ee
lint
CyrusNajmabadi Apr 4, 2024
4db1689
Merge remote-tracking branch 'upstream/main' into frozenPartialTagging
CyrusNajmabadi Apr 5, 2024
54485a1
Use disposal source
CyrusNajmabadi Apr 5, 2024
bb87d24
Switch to any
CyrusNajmabadi Apr 5, 2024
f4a0212
simplify logic
CyrusNajmabadi Apr 5, 2024
ba2fb3a
move option lower, and inline method
CyrusNajmabadi Apr 5, 2024
2717585
do the same for highlighting
CyrusNajmabadi Apr 5, 2024
5ae41b5
voidresult
CyrusNajmabadi Apr 5, 2024
05d0c6f
Update src/EditorFeatures/Core/ReferenceHighlighting/ReferenceHighlig…
CyrusNajmabadi Apr 5, 2024
df8d68a
voidresult
CyrusNajmabadi Apr 5, 2024
492c3fc
Merge branch 'frozenPartialTagging' of https://github.com/CyrusNajmab…
CyrusNajmabadi Apr 5, 2024
f31e40f
Simplify
CyrusNajmabadi Apr 5, 2024
6e8da4c
REvert
CyrusNajmabadi Apr 5, 2024
b78d0a8
revert
CyrusNajmabadi Apr 5, 2024
c58a604
remove redundant check
CyrusNajmabadi Apr 5, 2024
a9b19d3
Docs
CyrusNajmabadi Apr 5, 2024
0e8d413
Update src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProv…
CyrusNajmabadi Apr 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ private static async Task<IEnumerable<ITagSpan<BraceHighlightTag>>> ProduceTagsA

var context = new TaggerContext<BraceHighlightTag>(
buffer.CurrentSnapshot.GetRelatedDocumentsWithChanges().FirstOrDefault(),
buffer.CurrentSnapshot, new SnapshotPoint(buffer.CurrentSnapshot, position));
buffer.CurrentSnapshot,
frozenPartialSemantics: false,
new SnapshotPoint(buffer.CurrentSnapshot, position));
await producer.GetTestAccessor().ProduceTagsAsync(context);

return context.TagSpans;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,30 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.CodeAnalysis.Workspaces;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Classification;

Expand Down Expand Up @@ -53,6 +61,28 @@ protected AbstractSemanticOrEmbeddedClassificationViewTaggerProvider(

protected sealed override TaggerDelay EventChangeDelay => TaggerDelay.Short;

/// <summary>
/// We do classification in two passes. In the first pass we do not block getting classifications on building the
/// full compilation. This may take a significant amount of time and can cause a very latency sensitive operation
/// (copying) to block the user while we wait on this work to happen.
/// <para>
/// It's also a better experience to get classifications to the user faster versus waiting a potentially large
/// amount of time waiting for all the compilation information to be built. For example, we can classify types that
/// we've parsed in other files, or partially loaded from metadata, even if we're still parsing/loading. For cross
/// language projects, this also produces semantic classifications more quickly as we do not have to wait on
/// skeletons to be built.
/// </para>
/// <para>
/// In the second pass though, we will go and do things without frozen-partial semantics, so that we do always snap
/// to a final correct state. Note: the expensive second pass will be kicked down the road as new events come in to
/// classify things.
/// </para>
/// </summary>
protected sealed override bool SupportsFrozenPartialSemantics => true;

protected override bool TagEquals(IClassificationTag tag1, IClassificationTag tag2)
=> tag1.ClassificationType.Classification == tag2.ClassificationType.Classification;

protected sealed override ITaggerEventSource CreateEventSource(ITextView textView, ITextBuffer subjectBuffer)
{
this.ThreadingContext.ThrowIfNotOnUIThread();
Expand All @@ -67,36 +97,181 @@ protected sealed override ITaggerEventSource CreateEventSource(ITextView textVie
TaggerEventSources.OnGlobalOptionChanged(_globalOptions, ClassificationOptionsStorage.ClassifyObsoleteSymbols));
}

protected sealed override Task ProduceTagsAsync(
protected sealed override async Task ProduceTagsAsync(
TaggerContext<IClassificationTag> context, DocumentSnapshotSpan spanToTag, CancellationToken cancellationToken)
{
var document = spanToTag.Document;
if (document == null)
return Task.CompletedTask;
return;

// Attempt to get a classification service which will actually produce the results.
// If we can't (because we have no Document, or because the language doesn't support
// this service), then bail out immediately.
var classificationService = document.GetLanguageService<IClassificationService>();
if (classificationService == null)
return Task.CompletedTask;
return;

// The LSP client will handle producing tags when running under the LSP editor.
// Our tagger implementation should return nothing to prevent conflicts.
var workspaceContextService = document.Project.Solution.Services.GetRequiredService<IWorkspaceContextService>();
if (workspaceContextService?.IsInLspEditorContext() == true)
return Task.CompletedTask;
return;

// If the LSP semantic tokens feature flag is enabled, return nothing to prevent conflicts.
var isLspSemanticTokensEnabled = _globalOptions.GetOption(LspOptionsStorage.LspSemanticTokensFeatureFlag);
if (isLspSemanticTokensEnabled)
return Task.CompletedTask;
return;

var classificationOptions = _globalOptions.GetClassificationOptions(document.Project.Language);
return ClassificationUtilities.ProduceTagsAsync(
context, spanToTag, classificationService, _typeMap, classificationOptions, _type, cancellationToken);
await ProduceTagsAsync(
context, spanToTag, classificationService, classificationOptions, cancellationToken).ConfigureAwait(false);
}

protected override bool TagEquals(IClassificationTag tag1, IClassificationTag tag2)
=> tag1.ClassificationType.Classification == tag2.ClassificationType.Classification;
public async Task ProduceTagsAsync(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are just method moves from a utilities class far awqay. they were only called here (and referred to tagger types). so i just moved them all back over. they are otherwise unchanged.

CyrusNajmabadi marked this conversation as resolved.
Show resolved Hide resolved
TaggerContext<IClassificationTag> context,
DocumentSnapshotSpan spanToTag,
IClassificationService classificationService,
ClassificationOptions options,
CancellationToken cancellationToken)
{
var document = spanToTag.Document;
if (document == null)
return;

var classified = await TryClassifyContainingMemberSpanAsync(
context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false);
if (classified)
{
return;
}

// We weren't able to use our specialized codepaths for semantic classifying.
// Fall back to classifying the full span that was asked for.
await ClassifySpansAsync(
context, document, spanToTag.SnapshotSpan, classificationService, options, cancellationToken).ConfigureAwait(false);
}

private async Task<bool> TryClassifyContainingMemberSpanAsync(
TaggerContext<IClassificationTag> context,
Document document,
SnapshotSpan snapshotSpan,
IClassificationService classificationService,
ClassificationOptions options,
CancellationToken cancellationToken)
{
var range = context.TextChangeRange;
if (range == null)
{
// There was no text change range, we can't just reclassify a member body.
return false;
}

// there was top level edit, check whether that edit updated top level element
if (!document.SupportsSyntaxTree)
return false;

var lastSemanticVersion = (VersionStamp?)context.State;
if (lastSemanticVersion != null)
{
var currentSemanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false);
if (lastSemanticVersion.Value != currentSemanticVersion)
{
// A top level change was made. We can't perform this optimization.
return false;
}
}

var service = document.GetRequiredLanguageService<ISyntaxFactsService>();

// perf optimization. Check whether all edits since the last update has happened within
// a member. If it did, it will find the member that contains the changes and only refresh
// that member. If possible, try to get a speculative binder to make things even cheaper.

var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

var changedSpan = new TextSpan(range.Value.Span.Start, range.Value.NewLength);
var member = service.GetContainingMemberDeclaration(root, changedSpan.Start);
if (member == null || !member.FullSpan.Contains(changedSpan))
{
// The edit was not fully contained in a member. Reclassify everything.
return false;
}

var memberBodySpan = service.GetMemberBodySpanForSpeculativeBinding(member);
if (memberBodySpan.IsEmpty)
{
// Wasn't a member we could reclassify independently.
return false;
}

// TODO(cyrusn): Unclear what this logic is for. It looks like it's just trying to narrow the span down
// slightly from the full member, just to its body. Unclear if this provides any substantive benefits. But
// keeping for now to preserve long standing logic.
var memberSpanToClassify = memberBodySpan.Contains(changedSpan)
? memberBodySpan.ToSpan()
: member.FullSpan.ToSpan();

// Take the subspan we know we want to classify, and intersect that with the actual span being asked for.
// That way if we're only asking for a portion of a method, we still only classify that, and not the whole
// method.
var finalSpanToClassify = memberSpanToClassify.Intersection(snapshotSpan.Span);
if (finalSpanToClassify is null)
return false;

var subSpanToTag = new SnapshotSpan(snapshotSpan.Snapshot, finalSpanToClassify.Value);

// re-classify only the member we're inside.
await ClassifySpansAsync(
context, document, subSpanToTag, classificationService, options, cancellationToken).ConfigureAwait(false);
return true;
}

private async Task ClassifySpansAsync(
TaggerContext<IClassificationTag> context,
Document document,
SnapshotSpan snapshotSpan,
IClassificationService classificationService,
ClassificationOptions options,
CancellationToken cancellationToken)
{
try
{
using (Logger.LogBlock(FunctionId.Tagger_SemanticClassification_TagProducer_ProduceTags, cancellationToken))
{
using var _ = Classifier.GetPooledList(out var classifiedSpans);

// Ensure that if we're producing tags for frozen/partial documents, that we pass along that info so
// that we preserve that same behavior in OOP if we end up computing the tags there.
options = options with { FrozenPartialSemantics = context.FrozenPartialSemantics };

if (_type == ClassificationType.Semantic)
{
await classificationService.AddSemanticClassificationsAsync(
document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false);
}
else if (_type == ClassificationType.EmbeddedLanguage)
{
await classificationService.AddEmbeddedLanguageClassificationsAsync(
document, snapshotSpan.Span.ToTextSpan(), options, classifiedSpans, cancellationToken).ConfigureAwait(false);
}
else
{
throw ExceptionUtilities.UnexpectedValue(_type);
}

foreach (var span in classifiedSpans)
context.AddTag(ClassificationUtilities.Convert(_typeMap, snapshotSpan.Snapshot, span));

var version = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false);

// Let the context know that this was the span we actually tried to tag.
context.SetSpansTagged([snapshotSpan]);
context.State = version;
}
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
}
Loading
Loading