Skip to content

Commit

Permalink
Merge pull request #64802 from CyrusNajmabadi/tagEquality
Browse files Browse the repository at this point in the history
Implemen equality semantics for async tagger tags.
  • Loading branch information
CyrusNajmabadi authored Oct 19, 2022
2 parents 8ed28cd + 1647dce commit c9118d3
Show file tree
Hide file tree
Showing 30 changed files with 375 additions and 72 deletions.
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
/// 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>
/// Doing direct equality 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 TagEquals(InlineDiagnosticsTag tag1, InlineDiagnosticsTag tag2)
=> tag1 == tag2;
}
}
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 TagEquals(LineSeparatorTag tag1, LineSeparatorTag tag2)
=> tag1 == tag2;
}
}
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 TagEquals(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 TagEquals(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 TagEquals(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>
{
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() &&
this._data.Id == other._data.Id &&
this._data.Message == other._data.Message;
}

public override int GetHashCode()
=> throw ExceptionUtilities.Unreachable();
}
}
}
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 TagEquals(ClassificationTag tag1, ClassificationTag tag2)
=> tag1.ClassificationType.Classification == tag2.ClassificationType.Classification;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
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;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics
{
Expand Down Expand Up @@ -73,7 +75,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 +128,12 @@ protected internal override bool IncludeDiagnostic(DiagnosticData diagnostic)
return PredefinedErrorTypeNames.OtherError;
}
}

protected override bool TagEquals(IErrorTag tag1, IErrorTag tag2)
{
Contract.ThrowIfFalse(tag1 is RoslynErrorTag);
Contract.ThrowIfFalse(tag2 is RoslynErrorTag);
return tag1.Equals(tag2);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Diagnostics
{
Expand Down Expand Up @@ -56,14 +57,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 TagEquals(IErrorTag tag1, IErrorTag tag2)
{
Contract.ThrowIfFalse(tag1 is RoslynErrorTag);
Contract.ThrowIfFalse(tag2 is RoslynErrorTag);
return tag1.Equals(tag2);
}
}
}
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 TagEquals(ITextMarkerTag tag1, ITextMarkerTag tag2)
{
Contract.ThrowIfFalse(tag1 == tag2, "ActiveStatementTag is a supposed to be a singleton");
return true;
}
}
}
Loading

0 comments on commit c9118d3

Please sign in to comment.