チュートリアル: エディター拡張機能でシェル コマンドを使用する
VSPackage から、メニュー コマンドなどの機能をエディターに追加できます。 このチュートリアルでは、メニュー コマンドを呼び出して、エディターのテキスト ビューに表示要素を追加する方法について説明します。
このチュートリアルでは、Managed Extensibility Framework (MEF) コンポーネント パーツと共に VSPackage を使用する方法について説明します。 VSPackage を使用して、メニュー コマンドを Visual Studio シェルに登録する必要があります。 また、コマンドを使用して、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 拡張機能を参照する
MEF コンポーネントを VSPackage に追加しているので、マニフェストで両方の種類のアセットを指定する必要があります。
Note
MEF の詳細については、「Managed Extensibility Framework (MEF)」を参照してださい。
VSPackage プロジェクトで MEF コンポーネントを参照するには
MenuCommandTest プロジェクトにおいて、source.extension.vsixmanifest ファイルを VSIX マニフェスト エディターで開きます。
[アセット]タブで、[新規作成]をクリックします。
[種類] の一覧で、[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
ITrackingSpan、作成者、説明に対応した 3 つのフィールドを
CommentAdornment
クラスに追加します。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 は、ビュー作成イベントをリッスンするために使用できる MED コンポーネント パーツです。
CommentAdornmentTest プロジェクトにクラス ファイルを追加して、
Connector
という名前を付けます。次の
using
ディレクティブを追加します。using System.ComponentModel.Composition; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities;
IWpfTextViewCreationListener を実装するクラスを宣言し、ContentTypeAttribute として "text"、TextViewRoleAttribute として Document を指定してそれをエクスポートします。 コンテンツ タイプ属性は、コンポーネントが適用されるコンテンツの種類を指定します。 テキスト型は、すべての非バイナリ ファイルの種類の基本データ型です。 したがって、作成されるほぼすべてのテキスト ビューがこの型になります。 テキスト ビューの Role 属性により、コンポーネントが適用されるテキスト ビューの種類が指定されます。 ドキュメント テキスト ビュー ロールでは、通常、行から構成され、ファイルに格納されるテキストを示します。
CommentAdornmentManager
の静的Create()
イベントを呼び出すように、 TextViewCreated メソッドを実装します。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 と、この装飾レイヤーと他のテキスト ビュー レイヤー (テキスト、キャレット、選択) の Z オーダー関係を定義する OrderAttribute を使用してエクスポートします。[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); } }
メニュー コマンドを使用して、コメントの装飾を追加します
VSPackage の MenuItemCallback
メソッドを実装すると、メニュー コマンドを使用して、コメントの装飾を作成できます。
以下の参照を 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) { }
アクティブなビューを取得するコードを追加します。 アクティブな
IVsTextView
を取得するには、Visual Studio シェルのSVsTextManager
を取得する必要があります。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); }
AddAdornment コンストラクターの AddAdornment コマンドのハンドラーとして、AddAdornmentHandler メソッドを設定します。
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...