Implement equality semantics for async tagger tags.#64802
Implement equality semantics for async tagger tags.#64802CyrusNajmabadi merged 41 commits intodotnet:mainfrom
Conversation
| /// </para> | ||
| /// </summary> | ||
| protected override bool Equals(InlineDiagnosticsTag tag1, InlineDiagnosticsTag tag2) | ||
| => false; |
| /// comparisons here. | ||
| /// </summary> | ||
| protected override bool Equals(LineSeparatorTag tag1, LineSeparatorTag tag2) | ||
| => tag1 == tag2; |
There was a problem hiding this comment.
many simple tags just need something like this.
| // Map span1 over to span2 | ||
| var span1 = snapshotSpan1.TranslateTo(snapshotSpan2.Snapshot, _provider.SpanTrackingMode); | ||
| return span1.Span == snapshotSpan2.Span; | ||
| } |
There was a problem hiding this comment.
more complex tags have to normalize themselves to do the equality comparison properly.
| { | ||
| internal partial class AbstractDiagnosticsAdornmentTaggerProvider<TTag> | ||
| { | ||
| protected sealed class RoslynErrorTag : ErrorTag, IEquatable<RoslynErrorTag> |
There was a problem hiding this comment.
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.
| } | ||
|
|
||
| public override int GetHashCode() | ||
| => throw ExceptionUtilities.Unreachable(); |
There was a problem hiding this comment.
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.
src/EditorFeatures/Core/EditAndContinue/ActiveStatementTaggerProvider.cs
Outdated
Show resolved
Hide resolved
| /// <summary> | ||
| /// The snapshot this tag was created against. | ||
| /// </summary> | ||
| public readonly ITextSnapshot Snapshot; |
There was a problem hiding this comment.
needed to add this so we can compare tags created against different snapshots.
| return oldTagTree.GetSpans(snapshot).Except(tagSpansToInvalidate, comparer: this); | ||
| return oldTagTree.GetSpans(snapshot).Except( | ||
| spansToInvalidate.SelectMany(oldTagTree.GetIntersectingSpans), | ||
| comparer: this); |
There was a problem hiding this comment.
same as before, just no intermediary list computation.
| => obj is InheritanceMarginItem item && Equals(item); | ||
|
|
||
| public bool Equals(InheritanceMarginItem other) | ||
| => this.LineNumber == other.LineNumber && |
There was a problem hiding this comment.
@Cosifne It would be good to not have LineNumber be part of hte data side of this. It is positional, and shouldn't cause us to teardown/recreate tags. In other words, if a member moves down one line, we should still be able to use its existing tags insteading of having to remove hte old one and create a new one.
teh general pattern for this is to have ITagSpan in the tagger store the position, and the ITag itself it position-free.
There was a problem hiding this comment.
Should we get a bug tracking this?
jasonmalinowski
left a comment
There was a problem hiding this comment.
Approach seems generally fine but I do wish we'd just push the editor's tags for implement equality semantics than us doing it ourselves. Seems like it'd be better for other teams (C++, etc.) and also means if the editor ever adds additional properties we don't have to update, etc.
| public override bool Equals(object? obj) | ||
| => Equals(obj as StructureTag); | ||
|
|
||
| public bool Equals(StructureTag? other) |
There was a problem hiding this comment.
Can we ask the editor to implement equals on their side?
| public bool Equals(ITagSpan<TTag> x, ITagSpan<TTag> y) | ||
| => x.Span == y.Span && EqualityComparer<TTag>.Default.Equals(x.Tag, y.Tag); | ||
| public bool Equals(ITagSpan<TTag>? x, ITagSpan<TTag>? y) | ||
| => x != null && y != null && x.Span == y.Span && _dataSource.Equals(x.Tag, y.Tag); |
There was a problem hiding this comment.
Why not just have the underlying tab objects implement equality here? I get that'll require some editor team work, but it seems like that scales across multiple teams (C++ doesn't have to reimplement this).
There was a problem hiding this comment.
i found that too easy to miss. this way (being abstract) forced all impls to have to consider what to do here. :-/
src/EditorFeatures/Core/Tagging/AbstractAsynchronousTaggerProvider.cs
Outdated
Show resolved
Hide resolved
| => obj is InheritanceMarginItem item && Equals(item); | ||
|
|
||
| public bool Equals(InheritanceMarginItem other) | ||
| => this.LineNumber == other.LineNumber && |
There was a problem hiding this comment.
Should we get a bug tracking this?
| if (this.Hint.ReplacementTextChange != null && | ||
| other.Hint.ReplacementTextChange != null && | ||
| !_provider.SpanEquals(_snapshot, this.Hint.ReplacementTextChange.Value.Span, other._snapshot, other.Hint.ReplacementTextChange.Value.Span)) |
There was a problem hiding this comment.
Should this be grouped with the other replacement text change checks, or was this intentional to sort cheapest to most expensive?
There was a problem hiding this comment.
intentional for that reason.
|
|
||
| protected override bool Equals(IErrorTag tag1, IErrorTag tag2) | ||
| { | ||
| return tag1 is RoslynErrorTag errorTag1 && |
There was a problem hiding this comment.
Should this do a hard cast, since we don't expect error tags to not be a RoslynErrorTag?
There was a problem hiding this comment.
yes. that seems reasonable.
| { | ||
| return other != null && | ||
| this.ErrorType == other.ErrorType && | ||
| this._data.GetValidHelpLinkUri() == other._data.GetValidHelpLinkUri() && |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
| } | ||
|
|
||
| /// <summary> | ||
| /// TODO: is there anything we can do better here? Inline diagnostic tags are not really data, but more UI |
There was a problem hiding this comment.
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.
| /// tagging architecture. This value cannot be <see cref="SpanTrackingMode.Custom"/>. | ||
| /// </summary> | ||
| protected virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; | ||
| public virtual SpanTrackingMode SpanTrackingMode => SpanTrackingMode.EdgeExclusive; |
There was a problem hiding this comment.
There was a problem hiding this comment.
fixed in followup.
| public bool IsImplementation { get; } | ||
|
|
||
| public override int GetHashCode() | ||
| => throw ExceptionUtilities.Unreachable(); |
There was a problem hiding this comment.
There was a problem hiding this comment.
fixed in followup.
Fixes #64797
This is necessary so that we can properly diff old/new tags and tell the editor the minimal change made.