演练:显示灯泡建议

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

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

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

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

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

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

    light bulb

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

    light bulb preview

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

创建托管扩展性框架 (MEF) 项目

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

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

  3. 删除现有的类文件。

  4. 添加对项目的以下引用,并将“复制本地”设置为: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。 使用测试建议操作的名称ContentTypeAttribute“text”导出它。

    [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

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

  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 参数中返回该游标;否则,该 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() 显示一些操作。 在许多情况下,在前面GetSuggestedActions()调用,HasSuggestedActionsAsync()但情况并不总是如此。 例如,如果用户仅GetSuggestedActions()通过按 (Ctrl+ .) 调用灯泡操作。

    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. 不要忘记对将显示文本更改为“将'{0}'转换为小写”和调用ToLowerToUpper执行相同操作LowerCaseSuggestedAction

生成并测试代码

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

  1. 生成解决方案。

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

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

    test the light bulb

  4. 指向灯泡。 应会看到向下箭头。

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

    test light bulb, expanded

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