演练:将 shell 命令与编辑器扩展配合使用
在 VSPackage 中,可以将菜单命令等功能添加到编辑器中。 本演练演示如何通过调用菜单命令向编辑器中的文本视图添加装饰。
本演练演示如何将 VSPackage 与托管扩展性框架(MEF)组件部件结合使用。 必须使用 VSPackage 向 Visual Studio shell 注册菜单命令。 还可以使用命令访问 MEF 组件部件。
使用菜单命令创建扩展
创建一个 VSPackage,用于在“工具”菜单上放置名为“添加装饰”的菜单命令。
创建名为
MenuCommandTest
C# VSIX 的项目,并添加自定义命令项模板名称 AddAdornment。 有关详细信息,请参阅 使用菜单命令创建扩展。此时会打开名为 MenuCommandTest 的解决方案。 MenuCommandTestPackage 文件包含创建菜单命令的代码,并将其 放在“工具” 菜单上。 此时,该命令只会显示一个消息框。 后面的步骤将演示如何更改此项以显示注释装饰。
在 VSIX 清单编辑器中打开 source.extension.vsixmanifest 文件。 选项卡
Assets
应为名为 MenuCommandTest 的 Microsoft.VisualStudio.VsPackage 创建一行。保存并关闭 source.extension.vsixmanifest 文件。
将 MEF 扩展添加到命令扩展
在解决方案资源管理器中,右键单击解决方案节点,单击“添加”,然后单击“新建项目”。 在“添加新项目”对话框中,单击 Visual C# 下的“扩展性”,然后单击“VSIX 项目”。 将项目命名为
CommentAdornmentTest
。由于此项目将与强命名的 VSPackage 程序集交互,因此必须对程序集进行签名。 可以重复使用为 VSPackage 程序集创建的密钥文件。
打开项目属性并选择“ 签名 ”选项卡。
选择“ 对程序集进行签名”。
在 “选择强名称密钥文件”下,选择 为 MenuCommandTest 程序集生成的 Key.snk 文件。
请参阅 VSPackage 项目中的 MEF 扩展
由于要向 VSPackage 添加 MEF 组件,因此必须在清单中指定这两种类型的资产。
注意
有关 MEF 的详细信息,请参阅 Managed Extensibility Framework (MEF)。
引用 VSPackage 项目中的 MEF 组件
在 MenuCommandTest 项目中,在 VSIX 清单编辑器中打开 source.extension.vsixmanifest 文件。
在 “资产 ”选项卡上,单击“ 新建”。
在“类型”列表中,选择“Microsoft.VisualStudio.MefComponent” 。
在“源”列表中,选择“当前解决方案中的项目”。
在 “项目” 列表中,选择 “CommentAdornmentTest”。
保存并关闭 source.extension.vsixmanifest 文件。
确保 MenuCommandTest 项目具有对 CommentAdornmentTest 项目的引用。
在 CommentAdornmentTest 项目中,将项目设置为生成程序集。 在解决方案资源管理器中,选择项目并在“属性”窗口中查找“将生成输出复制到 OutputDirectory 属性”,并将其设置为 true。
定义注释装饰
注释装饰本身包含一个 ITrackingSpan 跟踪所选文本的字符串,以及一些表示作者和文本说明的字符串。
定义注释装饰
在 CommentAdornmentTest 项目中,添加新的类文件并将其命名
CommentAdornment
。添加以下引用:
Microsoft.VisualStudio.CoreUtility
Microsoft.VisualStudio.Text.Data
Microsoft.VisualStudio.Text.Logic
Microsoft.VisualStudio.Text.UI
Microsoft.VisualStudio.Text.UI.Wpf
System.ComponentModel.Composition
PresentationCore
PresentationFramework
WindowsBase
添加以下
using
指令。using Microsoft.VisualStudio.Text;
该文件应包含一个名为
CommentAdornment
..internal class CommentAdornment
向
CommentAdornment
类中添加三个字段,供 ITrackingSpan作者和说明使用。public readonly ITrackingSpan Span; public readonly string Author; public readonly string Text;
添加初始化字段的构造函数。
public CommentAdornment(SnapshotSpan span, string author, string text) { this.Span = span.Snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeExclusive); this.Author = author; this.Text = text; }
为装饰创建视觉元素
为装饰定义视觉元素。 在本演练中,定义继承自 Windows Presentation Foundation (WPF) 类 Canvas的控件。
在 CommentAdornmentTest 项目中创建一个类,并将其命名
CommentBlock
。添加以下
using
指令。using Microsoft.VisualStudio.Text; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
CommentBlock
使类继承自 Canvas.internal class CommentBlock : Canvas { }
添加一些专用字段以定义装饰的视觉方面。
private Geometry textGeometry; private Grid commentGrid; private static Brush brush; private static Pen solidPen; private static Pen dashPen;
添加一个构造函数,该构造函数定义注释装饰并添加相关文本。
public CommentBlock(double textRightEdge, double viewRightEdge, Geometry newTextGeometry, string author, string body) { if (brush == null) { brush = new SolidColorBrush(Color.FromArgb(0x20, 0x00, 0xff, 0x00)); brush.Freeze(); Brush penBrush = new SolidColorBrush(Colors.Green); penBrush.Freeze(); solidPen = new Pen(penBrush, 0.5); solidPen.Freeze(); dashPen = new Pen(penBrush, 0.5); dashPen.DashStyle = DashStyles.Dash; dashPen.Freeze(); } this.textGeometry = newTextGeometry; TextBlock tb1 = new TextBlock(); tb1.Text = author; TextBlock tb2 = new TextBlock(); tb2.Text = body; const int MarginWidth = 8; this.commentGrid = new Grid(); this.commentGrid.RowDefinitions.Add(new RowDefinition()); this.commentGrid.RowDefinitions.Add(new RowDefinition()); ColumnDefinition cEdge = new ColumnDefinition(); cEdge.Width = new GridLength(MarginWidth); ColumnDefinition cEdge2 = new ColumnDefinition(); cEdge2.Width = new GridLength(MarginWidth); this.commentGrid.ColumnDefinitions.Add(cEdge); this.commentGrid.ColumnDefinitions.Add(new ColumnDefinition()); this.commentGrid.ColumnDefinitions.Add(cEdge2); System.Windows.Shapes.Rectangle rect = new System.Windows.Shapes.Rectangle(); rect.RadiusX = 6; rect.RadiusY = 3; rect.Fill = brush; rect.Stroke = Brushes.Green; Size inf = new Size(double.PositiveInfinity, double.PositiveInfinity); tb1.Measure(inf); tb2.Measure(inf); double middleWidth = Math.Max(tb1.DesiredSize.Width, tb2.DesiredSize.Width); this.commentGrid.Width = middleWidth + 2 * MarginWidth; Grid.SetColumn(rect, 0); Grid.SetRow(rect, 0); Grid.SetRowSpan(rect, 2); Grid.SetColumnSpan(rect, 3); Grid.SetRow(tb1, 0); Grid.SetColumn(tb1, 1); Grid.SetRow(tb2, 1); Grid.SetColumn(tb2, 1); this.commentGrid.Children.Add(rect); this.commentGrid.Children.Add(tb1); this.commentGrid.Children.Add(tb2); Canvas.SetLeft(this.commentGrid, Math.Max(viewRightEdge - this.commentGrid.Width - 20.0, textRightEdge + 20.0)); Canvas.SetTop(this.commentGrid, textGeometry.GetRenderBounds(solidPen).Top); this.Children.Add(this.commentGrid); }
同时实现绘制 OnRender 装饰的事件处理程序。
protected override void OnRender(DrawingContext dc) { base.OnRender(dc); if (this.textGeometry != null) { dc.DrawGeometry(brush, solidPen, this.textGeometry); Rect textBounds = this.textGeometry.GetRenderBounds(solidPen); Point p1 = new Point(textBounds.Right, textBounds.Bottom); Point p2 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid) - 20.0, p1.X), p1.Y); Point p3 = new Point(Math.Max(Canvas.GetLeft(this.commentGrid), p1.X), (Canvas.GetTop(this.commentGrid) + p1.Y) * 0.5); dc.DrawLine(dashPen, p1, p2); dc.DrawLine(dashPen, p2, p3); } }
添加 IWpfTextViewCreationListener
IWpfTextViewCreationListener这是一个 MEF 组件部件,可用于侦听创建事件。
将类文件添加到 CommentAdornmentTest 项目并将其命名
Connector
。添加以下
using
指令。using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
声明实现IWpfTextViewCreationListener的类,并使用“text”和
ContentTypeAttribute/> 的Document类TextViewRoleAttribute导出。 内容类型属性指定组件应用到的内容类型。 文本类型是所有非二进制文件类型的基类型。 因此,创建的几乎所有文本视图都将是此类型。 文本视图角色属性指定组件应用到的文本视图的类型。 文档文本视图角色通常显示由行组成且存储在文件中的文本。 实现该方法TextViewCreated,以便调用该
CommentAdornmentManager
方法的静态Create()
事件。public void TextViewCreated(IWpfTextView textView) { CommentAdornmentManager.Create(textView); }
添加可用于执行命令的方法。
static public void Execute(IWpfTextViewHost host) { IWpfTextView view = host.TextView; //Add a comment on the selected text. if (!view.Selection.IsEmpty) { //Get the provider for the comment adornments in the property bag of the view. CommentAdornmentProvider provider = view.Properties.GetProperty<CommentAdornmentProvider>(typeof(CommentAdornmentProvider)); //Add some arbitrary author and comment text. string author = System.Security.Principal.WindowsIdentity.GetCurrent().Name; string comment = "Four score...."; //Add the comment adornment using the provider. provider.Add(view.Selection.SelectedSpans[0], author, comment); } }
定义装饰层
若要添加新装饰,必须定义装饰层。
定义装饰层
在类中
Connector
,声明一个类型 AdornmentLayerDefinition的公共字段,并使用指定装饰层的唯一名称导出它 NameAttribute ,以及 OrderAttribute 定义此装饰层与其他文本视图层(文本、插入点和选定内容)的 Z 顺序关系。[Export(typeof(AdornmentLayerDefinition))] [Name("CommentAdornmentLayer")] [Order(After = PredefinedAdornmentLayers.Selection, Before = PredefinedAdornmentLayers.Text)] public AdornmentLayerDefinition commentLayerDefinition;
提供注释装饰
定义装饰时,还实现注释装饰提供程序和注释装饰管理器。 注释修饰提供程序保留注释装饰列表,侦 Changed 听基础文本缓冲区上的事件,并在删除基础文本时删除注释装饰。
向 CommentAdornmentTest 项目添加新的类文件并将其命名
CommentAdornmentProvider
。添加以下
using
指令。using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor;
添加名为的
CommentAdornmentProvider
的类。internal class CommentAdornmentProvider { }
为文本缓冲区和与缓冲区相关的注释装饰列表添加专用字段。
private ITextBuffer buffer; private IList<CommentAdornment> comments = new List<CommentAdornment>();
为 .添加构造函数
CommentAdornmentProvider
此构造函数应具有专用访问权限,因为提供程序由Create()
该方法实例化。 构造函数将OnBufferChanged
事件处理程序添加到事件 Changed 。private CommentAdornmentProvider(ITextBuffer buffer) { this.buffer = buffer; //listen to the Changed event so we can react to deletions. this.buffer.Changed += OnBufferChanged; }
添加
Create()
方法。public static CommentAdornmentProvider Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentProvider>(delegate { return new CommentAdornmentProvider(view.TextBuffer); }); }
添加
Detach()
方法。public void Detach() { if (this.buffer != null) { //remove the Changed listener this.buffer.Changed -= OnBufferChanged; this.buffer = null; } }
OnBufferChanged
添加事件处理程序。private void OnBufferChanged(object sender, TextContentChangedEventArgs e) { //Make a list of all comments that have a span of at least one character after applying the change. There is no need to raise a changed event for the deleted adornments. The adornments are deleted only if a text change would cause the view to reformat the line and discard the adornments. IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { Span span = comment.Span.GetSpan(e.After); //if a comment does not span at least one character, its text was deleted. if (span.Length != 0) { keptComments.Add(comment); } } this.comments = keptComments; }
为事件添加声明
CommentsChanged
。public event EventHandler<CommentsChangedEventArgs> CommentsChanged;
创建用于
Add()
添加装饰的方法。public void Add(SnapshotSpan span, string author, string text) { if (span.Length == 0) throw new ArgumentOutOfRangeException("span"); if (author == null) throw new ArgumentNullException("author"); if (text == null) throw new ArgumentNullException("text"); //Create a comment adornment given the span, author and text. CommentAdornment comment = new CommentAdornment(span, author, text); //Add it to the list of comments. this.comments.Add(comment); //Raise the changed event. EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(comment, null)); }
添加方法
RemoveComments()
。public void RemoveComments(SnapshotSpan span) { EventHandler<CommentsChangedEventArgs> commentsChanged = this.CommentsChanged; //Get a list of all the comments that are being kept IList<CommentAdornment> keptComments = new List<CommentAdornment>(this.comments.Count); foreach (CommentAdornment comment in this.comments) { //find out if the given span overlaps with the comment text span. If two spans are adjacent, they do not overlap. To consider adjacent spans, use IntersectsWith. if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) { //Raise the change event to delete this comment. if (commentsChanged != null) commentsChanged(this, new CommentsChangedEventArgs(null, comment)); } else keptComments.Add(comment); } this.comments = keptComments; }
添加返回
GetComments()
给定快照范围中的所有注释的方法。public Collection<CommentAdornment> GetComments(SnapshotSpan span) { IList<CommentAdornment> overlappingComments = new List<CommentAdornment>(); foreach (CommentAdornment comment in this.comments) { if (comment.Span.GetSpan(span.Snapshot).OverlapsWith(span)) overlappingComments.Add(comment); } return new Collection<CommentAdornment>(overlappingComments); }
添加一个名为
CommentsChangedEventArgs>的类,如下所示。 internal class CommentsChangedEventArgs : EventArgs { public readonly CommentAdornment CommentAdded; public readonly CommentAdornment CommentRemoved; public CommentsChangedEventArgs(CommentAdornment added, CommentAdornment removed) { this.CommentAdded = added; this.CommentRemoved = removed; } }
管理注释装饰
注释装饰管理器创建装饰,并将其添加到装饰层。 它侦 LayoutChanged 听事件, Closed 以便它可以移动或删除装饰。 它还侦 CommentsChanged
听添加或删除注释时注释装饰提供程序触发的事件。
将类文件添加到 CommentAdornmentTest 项目并将其命名
CommentAdornmentManager
。添加以下
using
指令。using System; using System.Collections.Generic; using System.Windows.Media; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Formatting;
添加名为的
CommentAdornmentManager
的类。internal class CommentAdornmentManager { }
添加一些专用字段。
private readonly IWpfTextView view; private readonly IAdornmentLayer layer; private readonly CommentAdornmentProvider provider;
添加一个构造函数,用于订阅管理器LayoutChanged以及Closed
CommentsChanged
事件。 构造函数是私有的,因为管理器由静态Create()
方法实例化。private CommentAdornmentManager(IWpfTextView view) { this.view = view; this.view.LayoutChanged += OnLayoutChanged; this.view.Closed += OnClosed; this.layer = view.GetAdornmentLayer("CommentAdornmentLayer"); this.provider = CommentAdornmentProvider.Create(view); this.provider.CommentsChanged += OnCommentsChanged; }
Create()
添加获取提供程序或根据需要创建的方法。public static CommentAdornmentManager Create(IWpfTextView view) { return view.Properties.GetOrCreateSingletonProperty<CommentAdornmentManager>(delegate { return new CommentAdornmentManager(view); }); }
添加
CommentsChanged
处理程序。private void OnCommentsChanged(object sender, CommentsChangedEventArgs e) { //Remove the comment (when the adornment was added, the comment adornment was used as the tag). if (e.CommentRemoved != null) this.layer.RemoveAdornmentsByTag(e.CommentRemoved); //Draw the newly added comment (this will appear immediately: the view does not need to do a layout). if (e.CommentAdded != null) this.DrawComment(e.CommentAdded); }
添加 Closed 处理程序。
private void OnClosed(object sender, EventArgs e) { this.provider.Detach(); this.view.LayoutChanged -= OnLayoutChanged; this.view.Closed -= OnClosed; }
添加 LayoutChanged 处理程序。
private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { //Get all of the comments that intersect any of the new or reformatted lines of text. List<CommentAdornment> newComments = new List<CommentAdornment>(); //The event args contain a list of modified lines and a NormalizedSpanCollection of the spans of the modified lines. //Use the latter to find the comments that intersect the new or reformatted lines of text. foreach (Span span in e.NewOrReformattedSpans) { newComments.AddRange(this.provider.GetComments(new SnapshotSpan(this.view.TextSnapshot, span))); } //It is possible to get duplicates in this list if a comment spanned 3 lines, and the first and last lines were modified but the middle line was not. //Sort the list and skip duplicates. newComments.Sort(delegate(CommentAdornment a, CommentAdornment b) { return a.GetHashCode().CompareTo(b.GetHashCode()); }); CommentAdornment lastComment = null; foreach (CommentAdornment comment in newComments) { if (comment != lastComment) { lastComment = comment; this.DrawComment(comment); } } }
添加用于绘制注释的私有方法。
private void DrawComment(CommentAdornment comment) { SnapshotSpan span = comment.Span.GetSpan(this.view.TextSnapshot); Geometry g = this.view.TextViewLines.GetMarkerGeometry(span); if (g != null) { //Find the rightmost coordinate of all the lines that intersect the adornment. double maxRight = 0.0; foreach (ITextViewLine line in this.view.TextViewLines.GetTextViewLinesIntersectingSpan(span)) maxRight = Math.Max(maxRight, line.Right); //Create the visualization. CommentBlock block = new CommentBlock(maxRight, this.view.ViewportRight, g, comment.Author, comment.Text); //Add it to the layer. this.layer.AddAdornment(span, comment, block); } }
使用菜单命令添加注释装饰
可以通过实现 MenuItemCallback
VSPackage 的方法,使用菜单命令创建注释装饰。
将以下引用添加到 MenuCommandTest 项目:
Microsoft.VisualStudio.TextManager.Interop
Microsoft.VisualStudio.Editor
Microsoft.VisualStudio.Text.UI.Wpf
打开 AddAdornment.cs 文件并添加以下
using
指令。using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Editor; using CommentAdornmentTest;
Execute()
删除该方法并添加以下命令处理程序。private async void AddAdornmentHandler(object sender, EventArgs e) { }
添加代码以获取活动视图。 必须获取
SVsTextManager
Visual Studio shell 才能激活IVsTextView
。private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); }
如果此文本视图是编辑器文本视图的实例,则可以将其 IVsUserData 强制转换为接口,然后获取 IWpfTextViewHost 及其关联 IWpfTextView。 IWpfTextViewHost使用该方法调用
Connector.Execute()
该方法,该方法获取注释装饰提供程序并添加装饰。 命令处理程序现在应类似于以下代码:private async void AddAdornmentHandler(object sender, EventArgs e) { IVsTextManager txtMgr = (IVsTextManager) await ServiceProvider.GetServiceAsync(typeof(SVsTextManager)); IVsTextView vTextView = null; int mustHaveFocus = 1; txtMgr.GetActiveView(mustHaveFocus, null, out vTextView); IVsUserData userData = vTextView as IVsUserData; if (userData == null) { Console.WriteLine("No text view is currently open"); return; } IWpfTextViewHost viewHost; object holder; Guid guidViewHost = DefGuidList.guidIWpfTextViewHost; userData.GetData(ref guidViewHost, out holder); viewHost = (IWpfTextViewHost)holder; Connector.Execute(viewHost); }
将 AddAdornmentHandler 方法设置为 AddAdornment 构造函数中 AddAdornment 命令的处理程序。
private AddAdornment(AsyncPackage package, OleMenuCommandService commandService) { this.package = package ?? throw new ArgumentNullException(nameof(package)); commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); var menuCommandID = new CommandID(CommandSet, CommandId); var menuItem = new MenuCommand(this.AddAdornmentHandler, menuCommandID); commandService.AddCommand(menuItem); }
生成并测试代码
生成解决方案并开始调试。 应显示实验实例。
创建文本文件。 键入一些文本,然后选择它。
在 “工具” 菜单上,单击“ 调用添加装饰”。 气球应显示在文本窗口的右侧,并且应包含类似于以下文本的文本。
YourUserName
Fourscore...