Visual Studio 2022扩展代码折叠/展开时图标重复问题求助
解决VS2022扩展代码折叠/展开时图标重复显示问题
问题描述
开发Visual Studio 2022扩展时,实现了在if (auto的auto前渲染可点击图标的功能,但代码块折叠或展开时,图标会在所有匹配位置重复显示。
问题根源
- 未监听视图布局变更:代码折叠/展开属于视图布局变化,不会触发文本缓冲区变更事件(
BufferChanged),导致旧装饰未被清理,新装饰重复添加。 - 装饰标识不稳定:使用
SnapshotSpan作为装饰的唯一标识,折叠/展开会导致快照关联的视图位置变化,旧标识无法匹配新快照,无法判断装饰已存在。 - 全文本扫描低效:
GetTags方法扫描整个文本内容,而非仅可见区域,增加不必要的标签生成。
解决方案
1. 监听视图布局变更事件
给IntraTextAdornmentManager添加布局变更监听,确保折叠/展开时触发装饰清理与更新:
internal class IntraTextAdornmentManager : IDisposable { // ... 原有字段 ... public IntraTextAdornmentManager(IWpfTextView view, ITagAggregator<IntraTextAdornmentTag> tagAggregator) { _view = view; _tagAggregator = tagAggregator; _layer = view.GetAdornmentLayer("IconAdornment"); // 替换为行号+列的稳定标识字典 _activeAdornments = new Dictionary<Tuple<int, int>, UIElement>(); _tagAggregator.TagsChanged += OnTagsChanged; // 添加布局变更监听 _view.LayoutChanged += OnLayoutChanged; UpdateAdornments(); } private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { // 清理重新布局的行对应的装饰 foreach (var line in e.NewOrReformattedLines) { ClearAdornments(line.Extent); } UpdateAdornments(); } public void Dispose() { _tagAggregator.TagsChanged -= OnTagsChanged; _view.LayoutChanged -= OnLayoutChanged; // 清理所有剩余装饰 foreach (var element in _activeAdornments.Values) { _layer.RemoveAdornment(element); } _activeAdornments.Clear(); } // ... 原有方法 ... }
2. 使用稳定的装饰标识
将_activeAdornments的键改为行号+列的元组,避免快照变更导致的标识失效:
// 替换原有Dictionary定义 private readonly Dictionary<Tuple<int, int>, UIElement> _activeAdornments; // 修改UpdateAdornments中的存在性判断逻辑 private void UpdateAdornments() { if (_isUpdating) return; _isUpdating = true; try { var snapshot = _view.TextSnapshot; var visibleSpan = _view.TextViewLines.FormattedSpan; var tags = _tagAggregator.GetTags(new NormalizedSnapshotSpanCollection(visibleSpan)); foreach (var tagSpan in tags) { var spans = tagSpan.Span.GetSpans(snapshot); if (spans.Count == 0) continue; var snapshotSpan = spans[0]; if (!snapshotSpan.Snapshot.Equals(snapshot)) continue; // 生成稳定的行号+列标识 var line = snapshotSpan.Start.GetContainingLine(); var lineNumber = line.LineNumber; var column = snapshotSpan.Start.Position - line.Start.Position; var key = Tuple.Create(lineNumber, column); if (_activeAdornments.ContainsKey(key)) continue; var element = CloneElement(tagSpan.Tag.Adornment as UIElement); if (element == null) continue; var textViewLine = _view.TextViewLines.GetTextViewLineContainingBufferPosition(snapshotSpan.Start); if (textViewLine == null || !textViewLine.Snapshot.Equals(snapshot)) continue; var charBounds = textViewLine.GetCharacterBounds(snapshotSpan.Start); Canvas.SetLeft(element, charBounds.Left); Canvas.SetTop(element, charBounds.Top); // 添加装饰时记录稳定标识 _layer.AddAdornment(AdornmentPositioningBehavior.TextRelative, snapshotSpan, null, element, (tag, removed) => { _activeAdornments.Remove(key); }); _activeAdornments[key] = element; } } finally { _isUpdating = false; } } // 修改ClearAdornments方法,基于行范围清理 private void ClearAdornments(SnapshotSpan span) { var keysToRemove = new List<Tuple<int, int>>(); foreach (var key in _activeAdornments.Keys) { var targetLine = span.Snapshot.GetLineFromLineNumber(key.Item1); if (span.IntersectsWith(targetLine.Extent)) { keysToRemove.Add(key); } } foreach (var key in keysToRemove) { if (_activeAdornments.TryGetValue(key, out var element)) { _layer.RemoveAdornment(element); _activeAdornments.Remove(key); } } }
3. 优化Tagger的扫描范围
修改IconTagger的GetTags方法,仅扫描传入的可见跨度,减少不必要的标签生成:
public IEnumerable<ITagSpan<IntraTextAdornmentTag>> GetTags(NormalizedSnapshotSpanCollection spans) { if (spans.Count == 0 || _isDisposed) yield break; foreach (var span in spans) { var snapshot = span.Snapshot; var text = span.GetText(); int searchStart = 0; while ((searchStart = text.IndexOf(_searchText, searchStart, StringComparison.OrdinalIgnoreCase)) != -1) { // 计算绝对位置 var absoluteStart = span.Start.Position + searchStart; var autoIndex = absoluteStart + _searchText.IndexOf("auto", StringComparison.OrdinalIgnoreCase); var snapshotPos = new SnapshotPoint(snapshot, autoIndex); yield return new TagSpan<IntraTextAdornmentTag>(new SnapshotSpan(snapshotPos, 0), new IconTag(snapshotPos, HandleIconClick)); searchStart += _searchText.Length; } } }
完整修改后代码
using Microsoft.VisualStudio.ComponentModelHost; using Microsoft; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; using System; using System.ComponentModel.Composition; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows; using Task = System.Threading.Tasks.Task; namespace VSIXProject1 { [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] [Guid(VSIXProject1Package.PackageGuidString)] [ProvideAutoLoad(UIContextGuids.NoSolution, PackageAutoLoadFlags.BackgroundLoad)] public sealed class VSIXProject1Package : AsyncPackage { public const string PackageGuidString = "2f7c4434-b1bc-4263-a7e7-ec0beda87ac7"; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress) { await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); var componentModel = (IComponentModel)await GetServiceAsync(typeof(SComponentModel)); Assumes.Present(componentModel); var navigatorService = componentModel.GetService<ITextStructureNavigatorSelectorService>(); var formatMapService = componentModel.GetService<IEditorFormatMapService>(); Assumes.Present(navigatorService); Assumes.Present(formatMapService); } } public class IconTag : IntraTextAdornmentTag { private readonly SnapshotPoint _position; public IconTag(SnapshotPoint position, Action<SnapshotPoint> clickCallback) : base(CreateIconElement(position, clickCallback), null) { _position = position; } private static UIElement CreateIconElement(SnapshotPoint position, Action<SnapshotPoint> clickCallback) { var icon = new TextBlock { Text = "🖰", FontSize = 18, Foreground = new SolidColorBrush(Color.FromRgb(0x3, 0x7F, 0xCB)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 4, 0), Cursor = Cursors.Hand }; icon.MouseDown += (sender, e) => { e.Handled = true; clickCallback?.Invoke(position); }; return icon; } } internal class IconTagger : ITagger<IntraTextAdornmentTag>, IDisposable { private readonly ITextBuffer _buffer; private readonly string _searchText = "if (auto"; private bool _isDisposed; public event EventHandler<SnapshotSpanEventArgs> TagsChanged; public IconTagger(ITextBuffer buffer) { _buffer = buffer; _buffer.Changed += BufferChanged; } private void HandleIconClick(SnapshotPoint position) { if (_isDisposed) return; var line = position.GetContainingLine(); var lineNumber = line.LineNumber + 1; var column = position.Position - line.Start.Position + 1; MessageBox.Show($"Icon clicked at line {lineNumber}, column {column}"); } private void BufferChanged(object sender, TextContentChangedEventArgs e) { if (_isDisposed) return; TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(new SnapshotSpan(_buffer.CurrentSnapshot, 0, _buffer.CurrentSnapshot.Length))); } public IEnumerable<ITagSpan<IntraTextAdornmentTag>> GetTags(NormalizedSnapshotSpanCollection spans) { if (spans.Count == 0 || _isDisposed) yield break; foreach (var span in spans) { var snapshot = span.Snapshot; var text = span.GetText(); int searchStart = 0; while ((searchStart = text.IndexOf(_searchText, searchStart, StringComparison.OrdinalIgnoreCase)) != -1) { var absoluteStart = span.Start.Position + searchStart; var autoIndex = absoluteStart + _searchText.IndexOf("auto", StringComparison.OrdinalIgnoreCase); var snapshotPos = new SnapshotPoint(snapshot, autoIndex); yield return new TagSpan<IntraTextAdornmentTag>(new SnapshotSpan(snapshotPos, 0), new IconTag(snapshotPos, HandleIconClick)); searchStart += _searchText.Length; } } } public void Dispose() { if (!_isDisposed) { _isDisposed = true; _buffer.Changed -= BufferChanged; } } } internal class IntraTextAdornmentManager : IDisposable { private readonly IWpfTextView _view; private readonly ITagAggregator<IntraTextAdornmentTag> _tagAggregator; private readonly IAdornmentLayer _layer; private readonly Dictionary<Tuple<int, int>, UIElement> _activeAdornments; private bool _isUpdating; public IntraTextAdornmentManager(IWpfTextView view, ITagAggregator<IntraTextAdornmentTag> tagAggregator) { _view = view; _tagAggregator = tagAggregator; _layer = view.GetAdornmentLayer("IconAdornment"); _activeAdornments = new Dictionary<Tuple<int, int>, UIElement>(); _tagAggregator.TagsChanged += OnTagsChanged; _view.LayoutChanged += OnLayoutChanged; UpdateAdornments(); } private void OnTagsChanged(object sender, TagsChangedEventArgs e) { var spans = e.Span.GetSpans(_view.TextSnapshot); foreach (var span in spans) { ClearAdornments(span); } UpdateAdornments(); } private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { foreach (var line in e.NewOrReformattedLines) { ClearAdornments(line.Extent); } UpdateAdornments(); } private void UpdateAdornments() { if (_isUpdating) return; _isUpdating = true; try { var snapshot = _view.TextSnapshot; var visibleSpan = _view.TextViewLines.FormattedSpan; var tags = _tagAggregator.GetTags(new NormalizedSnapshotSpanCollection(visibleSpan)); foreach (var tagSpan in tags) { var spans = tagSpan.Span.GetSpans(snapshot); if (spans.Count == 0) continue; var snapshotSpan = spans[0]; if (!snapshotSpan.Snapshot.Equals(snapshot)) continue; var line = snapshotSpan.Start.GetContainingLine(); var lineNumber = line.LineNumber; var column = snapshotSpan.Start.Position - line.Start.Position; var key = Tuple.Create(lineNumber, column); if (_activeAdornments.ContainsKey(key)) continue; var element = CloneElement(tagSpan.Tag.Adornment as UIElement); if (element == null) continue; var textViewLine = _view.TextViewLines.GetTextViewLineContainingBufferPosition(snapshotSpan.Start); if (textViewLine == null || !textViewLine.Snapshot.Equals(snapshot)) continue; var charBounds = textViewLine.GetCharacterBounds(snapshotSpan.Start); Canvas.SetLeft(element, charBounds.Left); Canvas.SetTop(element, charBounds.Top); _layer.AddAdornment(AdornmentPositioningBehavior.TextRelative, snapshotSpan, null, element, (tag, removed) => { _activeAdornments.Remove(key); }); _activeAdornments[key] = element; } } finally { _isUpdating = false; } } private void ClearAdornments(SnapshotSpan span) { var keysToRemove = new List<Tuple<int, int>>(); foreach (var key in _activeAdornments.Keys) { var targetLine = span.Snapshot.GetLineFromLineNumber(key.Item1); if (span.IntersectsWith(targetLine.Extent)) { keysToRemove.Add(key); } } foreach (var key in keysToRemove) { if (_activeAdornments.TryGetValue(key, out var element)) { _layer.RemoveAdornment(element); _activeAdornments.Remove(key); } } } private static UIElement CloneElement(UIElement element) { if (element is TextBlock original) { return new TextBlock { Text = original.Text, FontSize = original.FontSize, Foreground = original.Foreground, VerticalAlignment = original.VerticalAlignment, Margin = original.Margin, Cursor = original.Cursor }; } return new TextBlock { Text = "🔧", FontSize = 14, Foreground = Brushes.Red, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(2, 0, 2, 0), Cursor = Cursors.Hand }; } public void Dispose() { _tagAggregator.TagsChanged -= OnTagsChanged; _view.LayoutChanged -= OnLayoutChanged; foreach (var element in _activeAdornments.Values) { _layer.RemoveAdornment(element); } _activeAdornments.Clear(); } } [Export(typeof(ITaggerProvider))] [ContentType("text")] [TagType(typeof(IntraTextAdornmentTag))] internal class IconTaggerProvider : ITaggerProvider { public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); return new IconTagger(buffer) as ITagger<T>; } } [Export(typeof(IWpfTextViewCreationListener))] [ContentType("text")] [TextViewRole(PredefinedTextViewRoles.Document)] public class IconAdornmentFactory
相关产品推荐
相关产品推荐

