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

Implement equality semantics for async tagger tags. #64802

Merged
merged 41 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9deff2c
Add tag equality comparers
CyrusNajmabadi Oct 18, 2022
7148d87
Add tag equality comparers
CyrusNajmabadi Oct 18, 2022
7fac8ca
Add tag equality comparers
CyrusNajmabadi Oct 18, 2022
48becee
Add tag equality comparers
CyrusNajmabadi Oct 18, 2022
5bf93ed
In progress
CyrusNajmabadi Oct 18, 2022
5f70bc4
Switch to simpler model
CyrusNajmabadi Oct 18, 2022
edc4588
Switch to simpler model
CyrusNajmabadi Oct 18, 2022
67489aa
Switch to simpler model
CyrusNajmabadi Oct 18, 2022
9d2e026
Fix
CyrusNajmabadi Oct 18, 2022
4bb2764
In progress
CyrusNajmabadi Oct 18, 2022
901959a
Testing
CyrusNajmabadi Oct 18, 2022
49a0641
Add back comparer
CyrusNajmabadi Oct 18, 2022
727c00c
simplify
CyrusNajmabadi Oct 18, 2022
3edfeee
simplify
CyrusNajmabadi Oct 18, 2022
a71492e
in progress
CyrusNajmabadi Oct 18, 2022
307060f
Add equality
CyrusNajmabadi Oct 18, 2022
4a33241
Continued equality
CyrusNajmabadi Oct 18, 2022
ea9a7e4
Doc
CyrusNajmabadi Oct 18, 2022
73d4c8b
Doc
CyrusNajmabadi Oct 18, 2022
e14f0d0
End to end
CyrusNajmabadi Oct 18, 2022
23c5089
Update src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerP…
CyrusNajmabadi Oct 18, 2022
9ad9b5d
REvert
CyrusNajmabadi Oct 18, 2022
eff6de0
Merge branch 'tagEquality' of https://github.com/CyrusNajmabadi/rosly…
CyrusNajmabadi Oct 18, 2022
a497bcd
Move complex code
CyrusNajmabadi Oct 18, 2022
ff54861
Add helper
CyrusNajmabadi Oct 18, 2022
343d0af
Add helper
CyrusNajmabadi Oct 18, 2022
5d5bdec
Use provider
CyrusNajmabadi Oct 18, 2022
71d07eb
Use provider
CyrusNajmabadi Oct 18, 2022
6c8c140
Use provider
CyrusNajmabadi Oct 18, 2022
0e3a38c
Use provider
CyrusNajmabadi Oct 18, 2022
c34bfc4
OBsolete
CyrusNajmabadi Oct 18, 2022
1cf970f
Rename
CyrusNajmabadi Oct 18, 2022
7e959bc
Doc
CyrusNajmabadi Oct 18, 2022
2b61433
Use a record instead
CyrusNajmabadi Oct 18, 2022
b2d3dee
Use a record instead
CyrusNajmabadi Oct 18, 2022
da1bef8
Add simplifying assumptions
CyrusNajmabadi Oct 18, 2022
2f451de
Add simplifying assumptions
CyrusNajmabadi Oct 18, 2022
0128f0e
Renames
CyrusNajmabadi Oct 18, 2022
deb93fa
rename
CyrusNajmabadi Oct 18, 2022
53d28dc
Merge remote-tracking branch 'upstream/main' into tagEquality
CyrusNajmabadi Oct 19, 2022
1647dce
Fix
CyrusNajmabadi Oct 19, 2022
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 @@ -121,5 +121,19 @@ diagnostic.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error &&
return null;
}
}

/// <summary>
/// TODO: is there anything we can do better here? Inline diagnostic tags are not really data, but more UI
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I think what the comment says makes sense, separating the data tagging and creating UI tags from the data tags. I would be happy to change that in a follow-up PR.

/// elements with specific constrols, positions and events attached to them. There doesn't seem to be a safe
/// way to reuse any of these currently. Ideally we could do something similar to inline-hints where there's a
/// data tagger portion (which is async and has clean equality semantics), and then the UI portion which just
/// translates those data-tags to the UI tags.
/// <para>
/// Returning false here means we'll always end up regenerating all tags. But hopefully there won't be that
/// many in a document to matter.
/// </para>
/// </summary>
protected override bool Equals(InlineDiagnosticsTag tag1, InlineDiagnosticsTag tag2)
=> false;
Copy link
Member Author

Choose a reason for hiding this comment

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

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,12 @@ protected override async Task ProduceTagsAsync(
context.AddTag(new TagSpan<LineSeparatorTag>(span.ToSnapshotSpan(snapshotSpan.Snapshot), tag));
}
}

/// <summary>
/// We create and cache a separator tag to use (unless the format mapping changes). So we can just use identity
/// comparisons here.
/// </summary>
protected override bool Equals(LineSeparatorTag tag1, LineSeparatorTag tag2)
=> tag1 == tag2;
Copy link
Member Author

Choose a reason for hiding this comment

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

many simple tags just need something like this.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,33 @@
// 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.Windows.Media;
using Microsoft.CodeAnalysis.Editor.Implementation.Adornments;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.StringIndentation
{
/// <summary>
/// Tag that specifies how a string's content is indented.
/// </summary>
internal class StringIndentationTag : BrushTag
internal class StringIndentationTag : BrushTag, IEquatable<StringIndentationTag>
{
private readonly StringIndentationTaggerProvider _provider;

public readonly ImmutableArray<SnapshotSpan> OrderedHoleSpans;

public StringIndentationTag(
StringIndentationTaggerProvider provider,
IEditorFormatMap editorFormatMap,
ImmutableArray<SnapshotSpan> orderedHoleSpans)
: base(editorFormatMap)
{
_provider = provider;
OrderedHoleSpans = orderedHoleSpans;
}

Expand All @@ -31,5 +37,28 @@ public StringIndentationTag(
var brush = view.VisualElement.TryFindResource("outlining.verticalrule.foreground") as SolidColorBrush;
return brush?.Color;
}

public override int GetHashCode()
=> throw ExceptionUtilities.Unreachable();

public override bool Equals(object? obj)
=> Equals(obj as StringIndentationTag);

public bool Equals(StringIndentationTag? other)
{
if (other is null)
return false;

if (this.OrderedHoleSpans.Length != other.OrderedHoleSpans.Length)
return false;

for (int i = 0, n = this.OrderedHoleSpans.Length; i < n; i++)
{
if (!_provider.SpanEquals(this.OrderedHoleSpans[i], other.OrderedHoleSpans[i]))
return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public StringIndentationTaggerProvider(
/// then the span of the tag will grow to the right and the line will immediately redraw in the correct position
/// while we're in the process of recomputing the up to date tags.
/// </summary>
protected override SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeInclusive;
public override SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeInclusive;

protected override ITaggerEventSource CreateEventSource(
ITextView? textView, ITextBuffer subjectBuffer)
Expand Down Expand Up @@ -113,9 +113,13 @@ protected override async Task ProduceTagsAsync(
context.AddTag(new TagSpan<StringIndentationTag>(
region.IndentSpan.ToSnapshotSpan(snapshot),
new StringIndentationTag(
this,
_editorFormatMap,
region.OrderedHoleSpans.SelectAsArray(s => s.ToSnapshotSpan(snapshot)))));
}
}

protected override bool Equals(StringIndentationTag tag1, StringIndentationTag tag2)
=> tag1.Equals(tag2);
}
}
6 changes: 3 additions & 3 deletions src/EditorFeatures/Core/BraceMatching/BraceHighlightTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.BraceMatching
{
internal class BraceHighlightTag : TextMarkerTag
internal sealed class BraceHighlightTag : TextMarkerTag
{
public static readonly BraceHighlightTag StartTag = new(navigateToStart: true);
public static readonly BraceHighlightTag EndTag = new(navigateToStart: false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,9 @@ private static void AddBraces(
context.AddTag(snapshot.GetTagSpan(braces.Value.RightSpan.ToSpan(), BraceHighlightTag.EndTag));
}
}

// Safe to directly compare as BraceHighlightTag uses singleton instances.
protected override bool Equals(BraceHighlightTag tag1, BraceHighlightTag tag2)
=> tag1 == tag2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Newtonsoft.Json;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Classification
Expand Down Expand Up @@ -124,5 +125,8 @@ protected sealed override Task ProduceTagsAsync(
return ClassificationUtilities.ProduceTagsAsync(
context, spanToTag, classificationService, _typeMap, classificationOptions, _type, cancellationToken);
}

protected override bool Equals(IClassificationTag tag1, IClassificationTag tag2)
=> tag1.ClassificationType.Classification == tag2.ClassificationType.Classification;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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 Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal partial class AbstractDiagnosticsAdornmentTaggerProvider<TTag>
{
protected sealed class RoslynErrorTag : ErrorTag, IEquatable<RoslynErrorTag>
Copy link
Member Author

Choose a reason for hiding this comment

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

by creating our own subclass, we can hold onto the data we used to create the actual ErrorTag. we can then use that data to compare old/new tags.

{
private readonly DiagnosticData _data;

public RoslynErrorTag(string errorType, Workspace workspace, DiagnosticData data)
: base(errorType, CreateToolTipContent(workspace, data))
{
_data = data;
}

private static object CreateToolTipContent(Workspace workspace, DiagnosticData diagnostic)
{
Action? navigationAction = null;
string? tooltip = null;
if (workspace != null)
{
var helpLinkUri = diagnostic.GetValidHelpLinkUri();
if (helpLinkUri != null)
{
navigationAction = new QuickInfoHyperLink(workspace, helpLinkUri).NavigationAction;
tooltip = diagnostic.HelpLink;
}
}

var diagnosticIdTextRun = navigationAction is null
? new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id)
: new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id, navigationAction, tooltip);

return new ContainerElement(
ContainerElementStyle.Wrapped,
new ClassifiedTextElement(
diagnosticIdTextRun,
new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Message)));
}

public override bool Equals(object? obj)
=> Equals(obj as RoslynErrorTag);

public bool Equals(RoslynErrorTag? other)
{
return other != null &&
this.ErrorType == other.ErrorType &&
this._data.GetValidHelpLinkUri() == other._data.GetValidHelpLinkUri() &&
Copy link
Member

Choose a reason for hiding this comment

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

Is this expensive? I see there's some URI parsing but if the only input is looks to be the descriptor's HelpLinkUri can we just compare that?

Copy link
Member Author

Choose a reason for hiding this comment

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

my goal was to have this match whatever we do when creating the actual final tag content. but i can def look into seeing about making all of them cheaper/conssitent

this._data.Id == other._data.Id &&
this._data.Message == other._data.Message;
}

public override int GetHashCode()
=> throw ExceptionUtilities.Unreachable();
Copy link
Member Author

Choose a reason for hiding this comment

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

while we implement Equals, we have no expectation that we will hash tags themselves. i explicitly made our tags throw so we catch if anything tries to do that.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@
// See the LICENSE file in the project root for more information.

using System;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Workspaces;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Tagging;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal abstract class AbstractDiagnosticsAdornmentTaggerProvider<TTag> :
internal abstract partial class AbstractDiagnosticsAdornmentTaggerProvider<TTag> :
AbstractDiagnosticsTaggerProvider<TTag>
where TTag : class, ITag
{
Expand Down Expand Up @@ -48,33 +45,6 @@ protected AbstractDiagnosticsAdornmentTaggerProvider(
return new TagSpan<TTag>(adjustedSpan, errorTag);
}

protected static object CreateToolTipContent(Workspace workspace, DiagnosticData diagnostic)
{
Action? navigationAction = null;
string? tooltip = null;
if (workspace != null)
{
var helpLinkUri = diagnostic.GetValidHelpLinkUri();
if (helpLinkUri != null)
{
navigationAction = new QuickInfoHyperLink(workspace, helpLinkUri).NavigationAction;
tooltip = diagnostic.HelpLink;
}
}

var diagnosticIdTextRun = navigationAction is null
? new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id)
: new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Id, navigationAction, tooltip);

return new ContainerElement(
ContainerElementStyle.Wrapped,
new ClassifiedTextElement(
diagnosticIdTextRun,
new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ":"),
new ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
new ClassifiedTextRun(ClassificationTypeNames.Text, diagnostic.Message)));
}

// By default, tags must have at least length '1' so that they can be visible in the UI layer.
protected virtual SnapshotSpan AdjustSnapshotSpan(SnapshotSpan span)
=> AdjustSnapshotSpan(span, minimumLength: 1, maximumLength: int.MaxValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,8 @@ protected internal override ImmutableArray<DiagnosticDataLocation> GetLocationsT
// Default to the base implementation for the diagnostic data
return base.GetLocationsToTag(diagnosticData);
}

protected override bool Equals(ClassificationTag tag1, ClassificationTag tag2)
=> tag1.ClassificationType.Classification == tag2.ClassificationType.Classification;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Workspaces;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
Expand Down Expand Up @@ -73,7 +74,7 @@ protected internal override bool IncludeDiagnostic(DiagnosticData diagnostic)
return null;
}

return new ErrorTag(errorType, CreateToolTipContent(workspace, diagnostic));
return new RoslynErrorTag(errorType, workspace, diagnostic);
}

private static string? GetErrorTypeFromDiagnostic(DiagnosticData diagnostic)
Expand Down Expand Up @@ -126,5 +127,12 @@ protected internal override bool IncludeDiagnostic(DiagnosticData diagnostic)
return PredefinedErrorTypeNames.OtherError;
}
}

protected override bool Equals(IErrorTag tag1, IErrorTag tag2)
{
return tag1 is RoslynErrorTag errorTag1 &&
Copy link
Member

Choose a reason for hiding this comment

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

Should this do a hard cast, since we don't expect error tags to not be a RoslynErrorTag?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes. that seems reasonable.

tag2 is RoslynErrorTag errorTag2 &&
errorTag1.Equals(errorTag2);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ protected internal override bool SupportsDignosticMode(DiagnosticMode mode)
}

protected override IErrorTag CreateTag(Workspace workspace, DiagnosticData diagnostic)
=> new ErrorTag(
PredefinedErrorTypeNames.HintedSuggestion,
CreateToolTipContent(workspace, diagnostic));
=> new RoslynErrorTag(PredefinedErrorTypeNames.HintedSuggestion, workspace, diagnostic);

protected override SnapshotSpan AdjustSnapshotSpan(SnapshotSpan snapshotSpan)
{
// We always want suggestion tags to be two characters long.
return AdjustSnapshotSpan(snapshotSpan, minimumLength: 2, maximumLength: 2);
}

protected override bool Equals(IErrorTag tag1, IErrorTag tag2)
{
return tag1 is RoslynErrorTag errorTag1 &&
tag2 is RoslynErrorTag errorTag2 &&
errorTag1.Equals(errorTag2);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Diagnostics;
Expand Down Expand Up @@ -101,5 +102,11 @@ protected override async Task ProduceTagsAsync(
// Let the context know that this was the span we actually tried to tag.
context.SetSpansTagged(ImmutableArray.Create(spanToTag.SnapshotSpan));
}

protected override bool Equals(ITextMarkerTag tag1, ITextMarkerTag tag2)
{
Contract.ThrowIfFalse(tag1 == tag2, "ActiveStatementTag is a supposed to be a singleton");
return true;
}
}
}
Loading