演练:显示灯泡建议

灯泡是 Visual Studio 编辑器中的图标,可展开以显示一组操作,例如,修复了内置代码分析器或代码重构所标识的问题。

在 Visual C# 和 Visual Basic 编辑器中,还可以使用 .NET 编译器平台(“Roslyn”)编写和打包自己的代码分析器,这些分析器配有可自动显示灯泡的操作。 有关详细信息,请参阅:

  • 如何:编写 C# 诊断和代码修复

  • 如何:编写 Visual Basic 诊断和代码修复

    其他语言(如 C++)还为某些快速操作(例如建议创建该函数的存根实现)提供灯泡。

    下面是灯泡的外观。 在 Visual Basic 或 Visual C# 项目中,当变量名称无效时,下方会出现红色波浪曲线。 如果将鼠标悬停在无效标识符上,光标附近会显示一个灯泡。

    灯泡

    如果通过灯泡单击向下箭头,将显示一组建议的操作,以及所选操作的预览。 在这种情况下,它会显示执行操作时对代码所做的更改。

    灯泡预览版

    可以使用灯泡来提供自己的建议操作。 例如,可以建议将左大括号移动到新行或将其移到上一行的末尾。 以下演示展示了如何创建一个在当前单词上显示的灯泡,并包含两个建议的操作:转换为大写转换为小写

创建 Managed Extensibility Framework (MEF) 项目

  1. 创建 C# VSIX 项目。 (在 新建项目 对话框中,选择 Visual C# /扩展性,然后 VSIX 项目。)将解决方案命名为 LightBulbTest

  2. 向项目添加 编辑器分类器 项模板。 有关详细信息,请参阅 使用编辑器项模板创建扩展

  3. 删除现有类文件。

  4. 将以下引用添加到项目,并将“Copy Local”设置为 False

    Microsoft.VisualStudio.Language.Intellisense

  5. 添加新的类文件并将其命名 LightBulbTest

  6. 添加以下 using 指令:

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Microsoft.VisualStudio.Language.Intellisense;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Operations;
    using Microsoft.VisualStudio.Utilities;
    using System.ComponentModel.Composition;
    using System.Threading;
    
    

实现灯泡源提供程序

  1. LightBulbTest.cs 类文件中,删除 LightBulbTest 类。 添加一个名为 TestSuggestedActionsSourceProvider 的类来实现 ISuggestedActionsSourceProvider。 导出它,并将名称设为“测试建议的操作”,将“text”设为 ContentTypeAttribute

    [Export(typeof(ISuggestedActionsSourceProvider))]
    [Name("Test Suggested Actions")]
    [ContentType("text")]
    internal class TestSuggestedActionsSourceProvider : ISuggestedActionsSourceProvider
    
  2. 在源提供程序类中,导入 ITextStructureNavigatorSelectorService 并将其添加为属性。

    [Import(typeof(ITextStructureNavigatorSelectorService))]
    internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }
    
  3. 实现 CreateSuggestedActionsSource 方法以返回 ISuggestedActionsSource 对象。 下一部分将讨论来源。

    public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer)
    {
        if (textBuffer == null || textView == null)
        {
            return null;
        }
        return new TestSuggestedActionsSource(this, textView, textBuffer);
    }
    

实现 ISuggestedActionSource

建议的操作源负责收集建议的操作集并将其添加到正确的上下文中。 在这种情况下,上下文是当前单词,建议的操作是 UpperCaseSuggestedActionLowerCaseSuggestedAction,下一节对此进行了讨论。

  1. 添加一个实现 ISuggestedActionsSource的类 TestSuggestedActionsSource

    internal class TestSuggestedActionsSource : ISuggestedActionsSource
    
  2. 为建议的操作源提供程序、文本缓冲区和文本视图添加专用只读字段。

    private readonly TestSuggestedActionsSourceProvider m_factory;
    private readonly ITextBuffer m_textBuffer;
    private readonly ITextView m_textView;
    
  3. 添加设置专用字段的构造函数。

    public TestSuggestedActionsSource(TestSuggestedActionsSourceProvider testSuggestedActionsSourceProvider, ITextView textView, ITextBuffer textBuffer)
    {
        m_factory = testSuggestedActionsSourceProvider;
        m_textBuffer = textBuffer;
        m_textView = textView;
    }
    
  4. 添加一个私有方法,该方法返回当前位于光标下的单词。 以下方法查看光标的当前位置,并请求文本结构导航器获取单词的范围信息。 如果光标位于单词上,则输出参数中返回 TextExtent;否则,out 参数 null,该方法返回 false

    private bool TryGetWordUnderCaret(out TextExtent wordExtent)
    {
        ITextCaret caret = m_textView.Caret;
        SnapshotPoint point;
    
        if (caret.Position.BufferPosition > 0)
        {
            point = caret.Position.BufferPosition - 1;
        }
        else
        {
            wordExtent = default(TextExtent);
            return false;
        }
    
        ITextStructureNavigator navigator = m_factory.NavigatorService.GetTextStructureNavigator(m_textBuffer);
    
        wordExtent = navigator.GetExtentOfWord(point);
        return true;
    }
    
  5. 实现 HasSuggestedActionsAsync 方法。 编辑器调用此方法来确定是否显示灯泡。 例如,每当光标从一行移动到另一行时,或者鼠标悬停在错误波形曲线上时,通常会进行此调用。 为了允许其他 UI 操作在方法处理过程中继续进行,此方法采用异步方式。 在大多数情况下,此方法需要对当前行执行一些分析和分析,因此处理可能需要一些时间。

    在此实现中,它以异步方式获取 TextExtent,并确定范围的大小是否显著,亦即,是否包含除空格之外的文本。

    public Task<bool> HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(() =>
        {
            TextExtent extent;
            if (TryGetWordUnderCaret(out extent))
            {
                // don't display the action if the extent has whitespace
                return extent.IsSignificant;
              }
            return false;
        });
    }
    
  6. 实现 GetSuggestedActions 方法,该方法返回包含不同 ISuggestedAction 对象的 SuggestedActionSet 对象的数组。 扩展灯泡时调用此方法。

    警告

    应确保 HasSuggestedActionsAsync()GetSuggestedActions() 的实现是一致的;也就是说,如果 HasSuggestedActionsAsync() 返回 true,则 GetSuggestedActions() 应显示一些操作。 在许多情况下,HasSuggestedActionsAsync()GetSuggestedActions()之前调用,但情况并不总是如此。 例如,如果用户通过按 (CTRL+ .) 调用灯泡操作,则只调用 GetSuggestedActions()

    public IEnumerable<SuggestedActionSet> GetSuggestedActions(ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, CancellationToken cancellationToken)
    {
        TextExtent extent;
        if (TryGetWordUnderCaret(out extent) && extent.IsSignificant)
        {
            ITrackingSpan trackingSpan = range.Snapshot.CreateTrackingSpan(extent.Span, SpanTrackingMode.EdgeInclusive);
            var upperAction = new UpperCaseSuggestedAction(trackingSpan);
            var lowerAction = new LowerCaseSuggestedAction(trackingSpan);
            return new SuggestedActionSet[] { new SuggestedActionSet(new ISuggestedAction[] { upperAction, lowerAction }) };
        }
        return Enumerable.Empty<SuggestedActionSet>();
    }
    
  7. 定义 SuggestedActionsChanged 事件。

    public event EventHandler<EventArgs> SuggestedActionsChanged;
    
  8. 若要完成实现,请为 Dispose()TryGetTelemetryId() 方法添加实现。 你不想执行遥测,因此只需返回 false 并将 GUID 设置为 Empty

    public void Dispose()
    {
    }
    
    public bool TryGetTelemetryId(out Guid telemetryId)
    {
        // This is a sample provider and doesn't participate in LightBulb telemetry
        telemetryId = Guid.Empty;
        return false;
    }
    

实现灯泡操作

  1. 在项目中,添加对 Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime.dll 的引用,并将 复制本地 设置为 False

  2. 创建两个类,第一个命名 UpperCaseSuggestedAction 和第二个命名 LowerCaseSuggestedAction。 两个类都实现 ISuggestedAction

    internal class UpperCaseSuggestedAction : ISuggestedAction
    internal class LowerCaseSuggestedAction : ISuggestedAction
    

    这两个类都是相同的,只是一个调用 ToUpper,另一个调用 ToLower。 以下步骤仅说明大写操作类,但你必须实现这两个类。 将实现大写操作的步骤用作实现小写操作的模式。

  3. 为这些类添加以下 using 指令:

    using Microsoft.VisualStudio.Imaging.Interop;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Media;
    
    
  4. 声明一组私有字段。

    private ITrackingSpan m_span;
    private string m_upper;
    private string m_display;
    private ITextSnapshot m_snapshot;
    
  5. 添加设置字段的构造函数。

    public UpperCaseSuggestedAction(ITrackingSpan span)
    {
        m_span = span;
        m_snapshot = span.TextBuffer.CurrentSnapshot;
        m_upper = span.GetText(m_snapshot).ToUpper();
        m_display = string.Format("Convert '{0}' to upper case", span.GetText(m_snapshot));
    }
    
  6. 实现 GetPreviewAsync 方法,使其显示操作预览。

    public Task<object> GetPreviewAsync(CancellationToken cancellationToken)
    {
        var textBlock = new TextBlock();
        textBlock.Padding = new Thickness(5);
        textBlock.Inlines.Add(new Run() { Text = m_upper });
        return Task.FromResult<object>(textBlock);
    }
    
  7. 实现 GetActionSetsAsync 方法,使其返回空的 SuggestedActionSet 枚举。

    public Task<IEnumerable<SuggestedActionSet>> GetActionSetsAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult<IEnumerable<SuggestedActionSet>>(null);
    }
    
  8. 按如下所示实现属性。

    public bool HasActionSets
    {
        get { return false; }
    }
    public string DisplayText
    {
        get { return m_display; }
    }
    public ImageMoniker IconMoniker
    {
       get { return default(ImageMoniker); }
    }
    public string IconAutomationText
    {
        get
        {
            return null;
        }
    }
    public string InputGestureText
    {
        get
        {
            return null;
        }
    }
    public bool HasPreview
    {
        get { return true; }
    }
    
  9. 通过将范围中的文本替换为其大写形式来实现 Invoke 方法。

    public void Invoke(CancellationToken cancellationToken)
    {
        m_span.TextBuffer.Replace(m_span.GetSpan(m_snapshot), m_upper);
    }
    

    警告

    灯泡操作 Invoke 方法不应显示 UI。 如果操作的确显示新的 UI(例如预览或“选择”对话框),请不要直接从 Invoke 方法内显示 UI,而是安排在从 Invoke 返回后显示 UI

  10. 若要完成实现,请添加 Dispose()TryGetTelemetryId() 方法。

    public void Dispose()
    {
    }
    
    public bool TryGetTelemetryId(out Guid telemetryId)
    {
        // This is a sample action and doesn't participate in LightBulb telemetry
        telemetryId = Guid.Empty;
        return false;
    }
    
  11. 别忘了也对 LowerCaseSuggestedAction 做同样的事情,将显示文本更改为“将 '{0}' 转换为小写”,并将调用 ToUpper 更改为 ToLower

生成并测试代码

若要测试此代码,请生成 LightBulbTest 解决方案并在实验实例中运行它。

  1. 生成解决方案。

  2. 在调试器中运行此项目时,将启动 Visual Studio 的第二个实例。

  3. 创建文本文件并键入某些文本。 文本左侧应会显示一个灯泡。

    测试灯泡

  4. 指向灯泡。 您应该会看到一个向下箭头。

  5. 单击灯泡时,应显示两个建议的操作以及所选操作的预览。

    测试灯泡,已展开

  6. 如果单击第一个操作,则当前单词中的所有文本都应转换为大写。 如果单击第二个操作,所有文本都应转换为小写。