Skip to content

Commit 6e26013

Browse files
Simplify inline hint tagging and fix duplicated tags. (#76525)
2 parents 074b1f3 + a1438f1 commit 6e26013

File tree

9 files changed

+181
-192
lines changed

9 files changed

+181
-192
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.VisualStudio.Text.Editor;
6+
using Microsoft.VisualStudio.Text.Formatting;
7+
using Microsoft.VisualStudio.Text.Tagging;
8+
9+
namespace Microsoft.CodeAnalysis.Editor.InlineHints;
10+
11+
internal partial class InlineHintsTaggerProvider
12+
{
13+
/// <summary>
14+
/// The computed adornment tag for an inline hint, along with information needed to determine if it can be reused.
15+
/// This is created and cached on <see cref="InlineHintDataTag{TAdditionalInformation}.AdditionalData"/> on demand
16+
/// so that we only create adornment tags once and reuse as long as possible.
17+
/// </summary>
18+
/// <param name="classified">Whether or not the adornment tag was classified. If the option for this changes, this
19+
/// cached tag should not be reused.</param>
20+
/// <param name="format">The text formatting used to create the hint. If this format no longer matches the current
21+
/// formatting, this should not be reused.</param>
22+
/// <param name="adornmentTagSpan">The actual adornment tag to render.</param>
23+
private sealed class CachedAdornmentTagSpan(
24+
bool classified,
25+
TextFormattingRunProperties format,
26+
TagSpan<IntraTextAdornmentTag> adornmentTagSpan)
27+
{
28+
public bool Classified { get; } = classified;
29+
public TextFormattingRunProperties Format { get; } = format;
30+
public TagSpan<IntraTextAdornmentTag> AdornmentTagSpan { get; } = adornmentTagSpan;
31+
}
32+
}

src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTagger.cs

Lines changed: 73 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,34 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Collections.Generic;
6+
using Microsoft.CodeAnalysis.Collections;
77
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
8+
using Microsoft.CodeAnalysis.Editor.Tagging;
89
using Microsoft.CodeAnalysis.ErrorReporting;
10+
using Microsoft.CodeAnalysis.Options;
11+
using Microsoft.CodeAnalysis.PooledObjects;
912
using Microsoft.CodeAnalysis.Text;
13+
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
14+
using Microsoft.CodeAnalysis.Utilities;
1015
using Microsoft.VisualStudio.Text;
1116
using Microsoft.VisualStudio.Text.Classification;
1217
using Microsoft.VisualStudio.Text.Editor;
1318
using Microsoft.VisualStudio.Text.Formatting;
1419
using Microsoft.VisualStudio.Text.Tagging;
1520
using Roslyn.Utilities;
1621

17-
namespace Microsoft.CodeAnalysis.Editor.InlineHints
22+
namespace Microsoft.CodeAnalysis.Editor.InlineHints;
23+
24+
internal partial class InlineHintsTaggerProvider
1825
{
1926
/// <summary>
20-
/// The purpose of this tagger is to convert the <see cref="InlineHintDataTag"/> to the <see
27+
/// The purpose of this tagger is to convert the <see cref="InlineHintDataTag{TAdditionalInformation}"/> to the <see
2128
/// cref="InlineHintsTag"/>, which actually creates the UIElement. It reacts to tags changing and updates the
2229
/// adornments accordingly.
2330
/// </summary>
24-
internal sealed class InlineHintsTagger : ITagger<IntraTextAdornmentTag>, IDisposable
31+
private sealed class InlineHintsTagger : EfficientTagger<IntraTextAdornmentTag>
2532
{
26-
private readonly ITagAggregator<InlineHintDataTag> _tagAggregator;
27-
28-
/// <summary>
29-
/// stores the parameter hint tags in a global location
30-
/// </summary>
31-
private readonly List<(IMappingTagSpan<InlineHintDataTag> mappingTagSpan, TagSpan<IntraTextAdornmentTag>? tagSpan)> _cache = [];
32-
33-
/// <summary>
34-
/// Stores the snapshot associated with the cached tags in <see cref="_cache" />
35-
/// </summary>
36-
private ITextSnapshot? _cacheSnapshot;
33+
private readonly EfficientTagger<InlineHintDataTag<CachedAdornmentTagSpan>> _underlyingTagger;
3734

3835
private readonly IClassificationFormatMap _formatMap;
3936

@@ -45,77 +42,59 @@ internal sealed class InlineHintsTagger : ITagger<IntraTextAdornmentTag>, IDispo
4542

4643
private readonly InlineHintsTaggerProvider _taggerProvider;
4744

48-
private readonly ITextBuffer _buffer;
4945
private readonly IWpfTextView _textView;
50-
51-
public event EventHandler<SnapshotSpanEventArgs>? TagsChanged;
46+
private readonly ITextBuffer _subjectBuffer;
5247

5348
public InlineHintsTagger(
5449
InlineHintsTaggerProvider taggerProvider,
5550
IWpfTextView textView,
56-
ITextBuffer buffer,
57-
ITagAggregator<InlineHintDataTag> tagAggregator)
51+
ITextBuffer subjectBuffer,
52+
EfficientTagger<InlineHintDataTag<CachedAdornmentTagSpan>> tagger)
5853
{
5954
_taggerProvider = taggerProvider;
6055

6156
_textView = textView;
62-
_buffer = buffer;
57+
_subjectBuffer = subjectBuffer;
58+
59+
// When the underlying tagger produced new data tags, inform any clients of us that we have new adornment tags.
60+
_underlyingTagger = tagger;
61+
_underlyingTagger.TagsChanged += OnTagsChanged;
6362

64-
_tagAggregator = tagAggregator;
6563
_formatMap = taggerProvider.ClassificationFormatMapService.GetClassificationFormatMap(textView);
6664
_hintClassification = taggerProvider.ClassificationTypeRegistryService.GetClassificationType(InlineHintsTag.TagId);
65+
6766
_formatMap.ClassificationFormatMappingChanged += this.OnClassificationFormatMappingChanged;
68-
_tagAggregator.BatchedTagsChanged += TagAggregator_BatchedTagsChanged;
67+
_taggerProvider.GlobalOptionService.AddOptionChangedHandler(this, OnGlobalOptionChanged);
6968
}
7069

71-
/// <summary>
72-
/// Goes through all the spans in which tags have changed and
73-
/// invokes a TagsChanged event. Using the BatchedTagsChangedEvent since it is raised
74-
/// on the same thread that created the tag aggregator, unlike TagsChanged.
75-
/// </summary>
76-
private void TagAggregator_BatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e)
70+
public override void Dispose()
7771
{
78-
_taggerProvider.ThreadingContext.ThrowIfNotOnUIThread();
79-
InvalidateCache();
80-
81-
var tagsChanged = TagsChanged;
82-
if (tagsChanged is null)
83-
{
84-
return;
85-
}
86-
87-
var mappingSpans = e.Spans;
88-
foreach (var item in mappingSpans)
89-
{
90-
var spans = item.GetSpans(_buffer);
91-
foreach (var span in spans)
92-
{
93-
if (tagsChanged != null)
94-
{
95-
tagsChanged.Invoke(this, new SnapshotSpanEventArgs(span));
96-
}
97-
}
98-
}
72+
_formatMap.ClassificationFormatMappingChanged -= OnClassificationFormatMappingChanged;
73+
_taggerProvider.GlobalOptionService.RemoveOptionChangedHandler(this, OnGlobalOptionChanged);
74+
_underlyingTagger.TagsChanged -= OnTagsChanged;
75+
_underlyingTagger.Dispose();
9976
}
10077

10178
private void OnClassificationFormatMappingChanged(object sender, EventArgs e)
10279
{
10380
_taggerProvider.ThreadingContext.ThrowIfNotOnUIThread();
81+
82+
// When classifications change we need to rebuild the inline tags with updated Font and Color information.
83+
10484
if (_format != null)
10585
{
10686
_format = null;
107-
InvalidateCache();
108-
109-
// When classifications change we need to rebuild the inline tags with updated Font and Color information.
110-
var tags = GetTags(new NormalizedSnapshotSpanCollection(_textView.TextViewLines.FormattedSpan));
111-
112-
foreach (var tag in tags)
113-
{
114-
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(tag.Span));
115-
}
87+
OnTagsChanged(this, new SnapshotSpanEventArgs(_subjectBuffer.CurrentSnapshot.GetFullSpan()));
11688
}
11789
}
11890

91+
private void OnGlobalOptionChanged(object sender, object target, OptionChangedEventArgs e)
92+
{
93+
// Reclassify everything.
94+
if (e.HasOption(option => option.Equals(InlineHintsViewOptionsStorage.ColorHints)))
95+
OnTagsChanged(this, new SnapshotSpanEventArgs(_subjectBuffer.CurrentSnapshot.GetFullSpan()));
96+
}
97+
11998
private TextFormattingRunProperties Format
12099
{
121100
get
@@ -126,86 +105,60 @@ private TextFormattingRunProperties Format
126105
}
127106
}
128107

129-
private void InvalidateCache()
130-
{
131-
_taggerProvider.ThreadingContext.ThrowIfNotOnUIThread();
132-
_cacheSnapshot = null;
133-
_cache.Clear();
134-
}
135-
136-
IEnumerable<ITagSpan<IntraTextAdornmentTag>> ITagger<IntraTextAdornmentTag>.GetTags(NormalizedSnapshotSpanCollection spans)
137-
=> GetTags(spans);
138-
139-
public IReadOnlyList<TagSpan<IntraTextAdornmentTag>> GetTags(NormalizedSnapshotSpanCollection spans)
108+
public override void AddTags(
109+
NormalizedSnapshotSpanCollection spans,
110+
SegmentedList<TagSpan<IntraTextAdornmentTag>> adornmentTagSpans)
140111
{
141112
try
142113
{
143114
if (spans.Count == 0)
144-
return [];
115+
return;
145116

117+
// If the snapshot has changed, we can't use any of the cached data, as it is associated with the
118+
// original snapshot they were created against.
146119
var snapshot = spans[0].Snapshot;
147-
if (snapshot != _cacheSnapshot)
148-
{
149-
// Calculate UI elements
150-
_cache.Clear();
151-
_cacheSnapshot = snapshot;
152-
153-
// Calling into the InlineParameterNameHintsDataTaggerProvider which only responds with the current
154-
// active view and disregards and requests for tags not in that view
155-
var fullSpan = new SnapshotSpan(snapshot, 0, snapshot.Length);
156-
var tags = _tagAggregator.GetTags(new NormalizedSnapshotSpanCollection(fullSpan));
157-
foreach (var tag in tags)
158-
{
159-
// Gets the associated span from the snapshot span and creates the IntraTextAdornmentTag from the data
160-
// tags. Only dealing with the dataTagSpans if the count is 1 because we do not see a multi-buffer case
161-
// occurring
162-
var dataTagSpans = tag.Span.GetSpans(snapshot);
163-
if (dataTagSpans.Count == 1)
164-
{
165-
_cache.Add((tag, tagSpan: null));
166-
}
167-
}
168-
}
169120

170121
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
171-
var classify = document != null && _taggerProvider.EditorOptionsService.GlobalOptions.GetOption(InlineHintsViewOptionsStorage.ColorHints, document.Project.Language);
122+
if (document is null)
123+
return;
124+
125+
var colorHints = _taggerProvider.EditorOptionsService.GlobalOptions.GetOption(InlineHintsViewOptionsStorage.ColorHints, document.Project.Language);
126+
127+
using var _1 = SegmentedListPool.GetPooledList<TagSpan<InlineHintDataTag<CachedAdornmentTagSpan>>>(out var dataTagSpans);
128+
_underlyingTagger.AddTags(spans, dataTagSpans);
129+
130+
// Presize so we can add the elements below without continually resizing.
131+
adornmentTagSpans.Capacity += dataTagSpans.Count;
132+
133+
using var _2 = PooledHashSet<int>.GetInstance(out var seenPositions);
172134

173-
var selectedSpans = new List<TagSpan<IntraTextAdornmentTag>>();
174-
for (var i = 0; i < _cache.Count; i++)
135+
var format = this.Format;
136+
foreach (var dataTagSpan in dataTagSpans)
175137
{
176-
var tagSpans = _cache[i].mappingTagSpan.Span.GetSpans(snapshot);
177-
if (tagSpans.Count == 1)
178-
{
179-
var tagSpan = tagSpans[0];
180-
if (spans.IntersectsWith(tagSpan))
181-
{
182-
if (_cache[i].tagSpan is not { } hintTagSpan)
183-
{
184-
var hintUITag = InlineHintsTag.Create(
185-
_cache[i].mappingTagSpan.Tag.Hint, Format, _textView, tagSpan, _taggerProvider, _formatMap, classify);
186-
187-
hintTagSpan = new TagSpan<IntraTextAdornmentTag>(tagSpan, hintUITag);
188-
_cache[i] = (_cache[i].mappingTagSpan, hintTagSpan);
189-
}
190-
191-
selectedSpans.Add(hintTagSpan);
192-
}
193-
}
138+
if (seenPositions.Add(dataTagSpan.Span.Start))
139+
adornmentTagSpans.Add(GetOrCreateAdornmentTagsSpan(dataTagSpan, colorHints, format));
194140
}
195-
196-
return selectedSpans;
197141
}
198142
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, ErrorSeverity.General))
199143
{
200144
throw ExceptionUtilities.Unreachable();
201145
}
202146
}
203147

204-
public void Dispose()
148+
private TagSpan<IntraTextAdornmentTag> GetOrCreateAdornmentTagsSpan(
149+
TagSpan<InlineHintDataTag<CachedAdornmentTagSpan>> dataTagSpan, bool classify, TextFormattingRunProperties format)
205150
{
206-
_tagAggregator.BatchedTagsChanged -= TagAggregator_BatchedTagsChanged;
207-
_tagAggregator.Dispose();
208-
_formatMap.ClassificationFormatMappingChanged -= OnClassificationFormatMappingChanged;
151+
// If we've never computed the adornment info, or options have changed, then compute and cache the new information.
152+
var cachedTagInformation = dataTagSpan.Tag.AdditionalData;
153+
if (cachedTagInformation is null || cachedTagInformation.Classified != classify || cachedTagInformation.Format != format)
154+
{
155+
var adornmentSpan = dataTagSpan.Span;
156+
cachedTagInformation = new(classify, format, new TagSpan<IntraTextAdornmentTag>(adornmentSpan, InlineHintsTag.Create(
157+
dataTagSpan.Tag.Hint, format, _textView, adornmentSpan, _taggerProvider, _formatMap, classify)));
158+
dataTagSpan.Tag.AdditionalData = cachedTagInformation;
159+
}
160+
161+
return cachedTagInformation.AdornmentTagSpan;
209162
}
210163
}
211164
}

0 commit comments

Comments
 (0)