You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Visual Studio 2022扩展代码折叠/展开时图标重复问题求助

解决VS2022扩展代码折叠/展开时图标重复显示问题

问题描述

开发Visual Studio 2022扩展时,实现了在if (auto的auto前渲染可点击图标的功能,但代码块折叠或展开时,图标会在所有匹配位置重复显示。

问题根源

  1. 未监听视图布局变更:代码折叠/展开属于视图布局变化,不会触发文本缓冲区变更事件(BufferChanged),导致旧装饰未被清理,新装饰重复添加。
  2. 装饰标识不稳定:使用SnapshotSpan作为装饰的唯一标识,折叠/展开会导致快照关联的视图位置变化,旧标识无法匹配新快照,无法判断装饰已存在。
  3. 全文本扫描低效: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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:09:48