Skip to content

Commit 1215d2e

Browse files
Optimize editor taggers and solution queries
Performance improvements to reduce unnecessary work: Tagger optimizations: - InternalTaggerBase now forwards only the actual changed span instead of invalidating the entire document on token tag changes - TokenTaggerBase.GetTags() filters tags by intersection testing, returning only tags overlapping the requested spans with proper snapshot translation - TokenTaggerBase.OnTagsUpdated() computes minimal bounding span of actual changes rather than invalidating the entire document Solution enumeration optimizations: - GetAllProjectsAsync() and GetProjectAsync() now resolve IVsHierarchyItemManager once upfront and use synchronous FromHierarchyItem() instead of awaiting FromHierarchyAsync() in each iteration, eliminating repeated async/await overhead These changes significantly reduce editor invalidation overhead and improve responsiveness, especially in large documents and solutions. [release]
1 parent 8624a25 commit 1215d2e

3 files changed

Lines changed: 88 additions & 10 deletions

File tree

src/toolkit/Community.VisualStudio.Toolkit.Shared/MEF/InternalTaggerBase.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,15 @@ public InternalTaggerBase(ITagAggregator<TokenTag>? tags)
3131

3232
private void TokenTagsChanged(object sender, TagsChangedEventArgs e)
3333
{
34-
ITextBuffer buffer = e.Span.BufferGraph.TopBuffer;
35-
SnapshotSpan span = new(buffer.CurrentSnapshot, 0, buffer.CurrentSnapshot.Length);
34+
IMappingSpan mappingSpan = e.Span;
35+
NormalizedSnapshotSpanCollection spans = mappingSpan.GetSpans(mappingSpan.BufferGraph.TopBuffer.CurrentSnapshot);
3636

37-
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(span));
37+
if (spans.Count > 0)
38+
{
39+
// Forward the actual changed span instead of manufacturing a full-document span
40+
SnapshotSpan changedSpan = new(spans[0].Start, spans[spans.Count - 1].End);
41+
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(changedSpan));
42+
}
3843
}
3944

4045
/// <inheritdoc/>

src/toolkit/Community.VisualStudio.Toolkit.Shared/MEF/TokenTaggerBase.cs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Threading.Tasks;
@@ -58,7 +58,29 @@ private void Tokenize(bool runFirstTokenizationImmediately)
5858
/// <inheritdoc/>
5959
public IEnumerable<ITagSpan<TokenTag>> GetTags(NormalizedSnapshotSpanCollection spans)
6060
{
61-
return TagsCache;
61+
if (spans.Count == 0)
62+
{
63+
yield break;
64+
}
65+
66+
List<ITagSpan<TokenTag>> cache = TagsCache;
67+
ITextSnapshot requestedSnapshot = spans[0].Snapshot;
68+
69+
foreach (ITagSpan<TokenTag> tagSpan in cache)
70+
{
71+
SnapshotSpan tagSnapshotSpan = tagSpan.Span;
72+
73+
// Translate the tag span to the requested snapshot if needed
74+
if (tagSnapshotSpan.Snapshot != requestedSnapshot)
75+
{
76+
tagSnapshotSpan = tagSnapshotSpan.TranslateTo(requestedSnapshot, SpanTrackingMode.EdgeExclusive);
77+
}
78+
79+
if (spans.IntersectsWith(tagSnapshotSpan))
80+
{
81+
yield return tagSpan;
82+
}
83+
}
6284
}
6385

6486
/// <summary>
@@ -102,9 +124,51 @@ public virtual Task<object> GetTooltipAsync(SnapshotPoint triggerPoint)
102124
/// <param name="tags"></param>
103125
protected void OnTagsUpdated(List<ITagSpan<TokenTag>> tags)
104126
{
127+
List<ITagSpan<TokenTag>> oldTags = TagsCache;
105128
TagsCache = tags;
106-
SnapshotSpan span = new(Buffer.CurrentSnapshot, 0, Buffer.CurrentSnapshot.Length);
107-
TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(span));
129+
130+
if (TagsChanged == null)
131+
{
132+
return;
133+
}
134+
135+
ITextSnapshot currentSnapshot = Buffer.CurrentSnapshot;
136+
137+
// Compute the minimal bounding span covering old and new tags
138+
int minStart = int.MaxValue;
139+
int maxEnd = int.MinValue;
140+
141+
ComputeBounds(oldTags, currentSnapshot, ref minStart, ref maxEnd);
142+
ComputeBounds(tags, currentSnapshot, ref minStart, ref maxEnd);
143+
144+
if (minStart <= maxEnd && minStart != int.MaxValue)
145+
{
146+
SnapshotSpan changedSpan = new(currentSnapshot, minStart, maxEnd - minStart);
147+
TagsChanged.Invoke(this, new SnapshotSpanEventArgs(changedSpan));
148+
}
149+
}
150+
151+
private static void ComputeBounds(List<ITagSpan<TokenTag>> tags, ITextSnapshot currentSnapshot, ref int minStart, ref int maxEnd)
152+
{
153+
foreach (ITagSpan<TokenTag> tagSpan in tags)
154+
{
155+
SnapshotSpan span = tagSpan.Span;
156+
157+
if (span.Snapshot != currentSnapshot)
158+
{
159+
span = span.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive);
160+
}
161+
162+
if (span.Start.Position < minStart)
163+
{
164+
minStart = span.Start.Position;
165+
}
166+
167+
if (span.End.Position > maxEnd)
168+
{
169+
maxEnd = span.End.Position;
170+
}
171+
}
108172
}
109173

110174
/// <inheritdoc/>

src/toolkit/Community.VisualStudio.Toolkit.Shared/Solution/Solutions.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Runtime.InteropServices;
@@ -86,11 +86,15 @@ public async Task<IEnumerable<Project>> GetAllProjectsAsync(ProjectStateFilter f
8686
IVsSolution solution = await VS.Services.GetSolutionAsync();
8787
IEnumerable<IVsHierarchy> hierarchies = solution.GetAllProjectHierarchies(filter);
8888

89+
// Resolve the hierarchy item manager once instead of per-iteration
90+
IVsHierarchyItemManager manager = await VS.GetMefServiceAsync<IVsHierarchyItemManager>();
91+
8992
List<Project> list = new();
9093

9194
foreach (IVsHierarchy hierarchy in hierarchies)
9295
{
93-
Project? proj = await SolutionItem.FromHierarchyAsync(hierarchy, VSConstants.VSITEMID_ROOT) as Project;
96+
IVsHierarchyItem hierItem = manager.GetHierarchyItem(hierarchy, VSConstants.VSITEMID_ROOT);
97+
Project? proj = SolutionItem.FromHierarchyItem(hierItem) as Project;
9498

9599
if (proj?.Type == SolutionItemType.Project)
96100
{
@@ -112,9 +116,14 @@ public async Task<IEnumerable<Project>> GetAllProjectsAsync(ProjectStateFilter f
112116
IVsSolution solution = await VS.Services.GetSolutionAsync();
113117
IEnumerable<IVsHierarchy> hierarchies = solution.GetAllProjectHierarchies(ProjectStateFilter.All);
114118

119+
// Resolve the hierarchy item manager once instead of per-iteration
120+
IVsHierarchyItemManager manager = await VS.GetMefServiceAsync<IVsHierarchyItemManager>();
121+
115122
foreach (IVsHierarchy hierarchy in hierarchies)
116123
{
117-
Project? proj = await SolutionItem.FromHierarchyAsync(hierarchy, VSConstants.VSITEMID_ROOT) as Project;
124+
IVsHierarchyItem hierItem = manager.GetHierarchyItem(hierarchy, VSConstants.VSITEMID_ROOT);
125+
Project? proj = SolutionItem.FromHierarchyItem(hierItem) as Project;
126+
118127
if (proj != null)
119128
{
120129
if (proj.Type == SolutionItemType.Project && string.Compare(proj.Name, projectName, true) == 0)

0 commit comments

Comments
 (0)