概要
Visual Studioを使用していて不満がでることもあると思います.
とはいえ, Vim, Emacsに比べIDEの拡張は難しいイメージがあります.
最近になって以下のようにドキュメントが充実してきていますが, 簡単な印象はありません.
簡単な例として, 文書保存時に改行コードを強制変換する拡張を作成していきます.
この文書が, あなたのVS拡張作成のとっかかりになればと思います.
プロジェクト作成
まずは, テンプレートから最低限の環境を用意します.
2015, 2017でインストールオプションによるでしょうが, C#カテゴリに"VSIX Project"があるでしょう.
プロジェクト名は, "ConvLF"とでもしておきます.
このままですと, ほぼ空のプロジェクトですので, 新しい項目の追加を行います.
"Visual Studio Package"テンプレートを追加します. ここまでで何もしないVS拡張ができるはずです.
デバッグ実行ができる状態になっていると思います. デバッグ実行すると"デバッグ用のVS"が立ち上がります.
いろいろなソフトウェアの拡張を開発してきた人はわかると思いますが, デバッグはかなり簡単になっています.
セーブイベントハンドラ
次に, "文書保存時"をハンドルしたいと思います.
テンプレートやサンプルにはそれらしいものがなさそうですので頑張ります, 既存のオープンソースを見たり, ドキュメントを漁るしかありません.
クラス"RunningDocTableEvents"を追加します.
ドキュメント(Document Table: 開いたドキュメントのビューに対応するオブジェクト)のイベントを受け取るために,
RunningDocTableEventsをIVsRunningDocTableEvents3の子にします.
抽象メソッドを実装すればそれっぽいOnBeforeSaveがあると思います. ここを変更すれば目的達成です.
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
namespace ConvLF
{
class RunningDocTableEvents : IVsRunningDocTableEvents3
{
private ConvLFPackage package_;
public RunningDocTableEvents(ConvLFPackage package)
{
package_ = package;
if(null != package_.RDT) {
package_.RDT.Advise(this);
}
}
public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
{
return VSConstants.S_OK;
}
public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining)
{
return VSConstants.S_OK;
}
public int OnAfterSave(uint docCookie)
{
return VSConstants.S_OK;
}
public int OnAfterAttributeChange(uint docCookie, uint grfAttribs)
{
return VSConstants.S_OK;
}
public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
{
return VSConstants.S_OK;
}
public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame)
{
return VSConstants.S_OK;
}
public int OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld, string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew)
{
return VSConstants.S_OK;
}
public int OnBeforeSave(uint docCookie)
{
return VSConstants.S_OK;
}
}
}
ConvLFPackageクラスにも, 適当に追加しておきます.
namespace ConvLF
{
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
[Guid(ConvLFPackage.PackageGuidString)]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
public sealed class ConvLFPackage : Package
{
public const string PackageGuidString = "5f4995c1-b01c-4e80-bc5e-f77eec24f6e0";
public EnvDTE80.DTE2 DTE { get { return dte2_.Value;} }
public RunningDocumentTable RDT { get { return runningDocumentTable_.Value;} }
private Lazy<EnvDTE80.DTE2> dte2_;
private Lazy<RunningDocumentTable> runningDocumentTable_;
private Lazy<Microsoft.VisualStudio.OLE.Interop.IServiceProvider> servicePorvider_;
private RunningDocTableEvents runningDocTableEvents_;
public ConvLFPackage()
{
}
#region Package Members
protected override void Initialize()
{
base.Initialize();
dte2_ = new Lazy<EnvDTE80.DTE2>(()=> GetService(typeof(EnvDTE.DTE)) as EnvDTE80.DTE2);
servicePorvider_ = new Lazy<Microsoft.VisualStudio.OLE.Interop.IServiceProvider>(() => Package.GetGlobalService(typeof(Microsoft.VisualStudio.OLE.Interop.IServiceProvider)) as Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
runningDocumentTable_ = new Lazy<RunningDocumentTable>(()=>new RunningDocumentTable(new ServiceProvider(servicePorvider_.Value)));
runningDocTableEvents_ = new RunningDocTableEvents(this);
}
#endregion
}
}
次にいくまえに, デバッグ用にデバッグprintを作ってしまいましょう.
出力ウィンドウに出力しています.
void Output(string message)
{
EnvDTE.OutputWindow outputWindow = package_.DTE.ToolWindows.OutputWindow;
if(null == outputWindow) {
return;
}
foreach(EnvDTE.OutputWindowPane window in outputWindow.OutputWindowPanes) {
window.OutputString(message);
}
}
改行変換
保存時に改行変換するために, OnBeforeSaveでドキュメントに変更を加えます.
とりあえず, これで改行変換はできました.
public int OnBeforeSave(uint docCookie)
{
RunningDocumentInfo runningDocumentInfo = package_.RDT.GetDocumentInfo(docCookie);
EnvDTE.Document document = package_.DTE.Documents.OfType<EnvDTE.Document>().SingleOrDefault(x => x.FullName == runningDocumentInfo.Moniker);
if(null == document) {
return VSConstants.S_OK;
}
if(document.Kind != "{8E7B96A8-E33D-11D0-A6D5-00C04FB67F6A}") {
return VSConstants.S_OK;
}
# if DEBUG
Output(document.Language + "\n");
# endif
EnvDTE.TextDocument textDocument = document.Object("TextDocument") as EnvDTE.TextDocument;
if(null == textDocument) {
return VSConstants.S_OK;
}
string replaceLineFeed = "\n";
int count = 0;
EnvDTE.EditPoint editPoint = textDocument.StartPoint.CreateEditPoint();
while(!editPoint.AtEndOfDocument) {
editPoint.EndOfLine();
string lineending = editPoint.GetText(1);
if(string.IsNullOrEmpty(lineending)) {
continue;
}
for(int i = 0; i < lineending.Length; ++i) {
if(lineending[i] != replaceLineFeed[0]) {
editPoint.ReplaceText(1, replaceLineFeed, 0);
++count;
}
}
editPoint.CharRight();
}
# if DEBUG
Output(string.Format("Replace {0} EOLs\n", count));
# endif
return VSConstants.S_OK;
}
設定
ここまでで, やりたいことはできましたが, もう少し汎用にしてみましょう.
設定に, 各言語ごとに改行コードを設定できるようにしてみます.
クラス"OptionPageConvLF"を追加します. 以下のように"Microsoft.VisualStudio.Shell.DialogPage"を継承して,
Visual Studioの設定にページを追加します.
using System.ComponentModel;
namespace ConvLF
{
public class OptionPageConvLF : Microsoft.VisualStudio.Shell.DialogPage
{
public enum TypeLineFeed
{
LF =0,
CR,
CRLF,
};
public enum TypeLanguage
{
[Description("C/C++")]
C_CPP,
[Description("CSharp")]
CSharp,
[Description("Others")]
Others,
};
private TypeLineFeed[] lineFeeds_ = new TypeLineFeed[3] { TypeLineFeed.LF, TypeLineFeed.LF, TypeLineFeed.LF};
[Category("General")]
[DisplayName("C/C++")]
[Description("Line feed for C/C++")]
public TypeLineFeed LineFeedCPP
{
get { return lineFeeds_[(int)TypeLanguage.C_CPP]; }
set { lineFeeds_[(int)TypeLanguage.C_CPP] = value; }
}
[Category("General")]
[DisplayName("CSharp")]
[Description("Line feed for CSharp")]
public TypeLineFeed LineFeedCSharp
{
get { return lineFeeds_[(int)TypeLanguage.CSharp]; }
set { lineFeeds_[(int)TypeLanguage.CSharp] = value; }
}
[Category("General")]
[DisplayName("Others")]
[Description("Line feed for Others")]
public TypeLineFeed LineFeedOthers
{
get { return lineFeeds_[(int)TypeLanguage.Others]; }
set { lineFeeds_[(int)TypeLanguage.Others] = value; }
}
}
}
さらに, ConvLFPackageのクラス属性にOptionPageConvLFを追加します.
Package経由でアクセスできるようにもしておきます.
[ProvideOptionPage(typeof(OptionPageConvLF), "OptionPageConvLF", "General", 0, 0, true)]
public sealed class ConvLFPackage : Package
{
public OptionPageConvLF Options
{
get
{
return GetDialogPage(typeof(OptionPageConvLF)) as OptionPageConvLF;
}
}
}
デバッグ実行すると, Visual Studioの設定ページに"OptionPageConvLF"が追加されているはずです.
設定を実装に反映します. 最終のOnBeforeSaveは以下になります.
変換が泥臭い処理なのは, その方が速かったからです. 保存するたびに1msでも待ちたくありませんから.
public int OnBeforeSave(uint docCookie)
{
RunningDocumentInfo runningDocumentInfo = package_.RDT.GetDocumentInfo(docCookie);
EnvDTE.Document document = package_.DTE.Documents.OfType<EnvDTE.Document>().SingleOrDefault(x => x.FullName == runningDocumentInfo.Moniker);
if(null == document) {
return VSConstants.S_OK;
}
if(document.Kind != "{8E7B96A8-E33D-11D0-A6D5-00C04FB67F6A}") {
return VSConstants.S_OK;
}
# if DEBUG
Output(document.Language + "\n");
# endif
EnvDTE.TextDocument textDocument = document.Object("TextDocument") as EnvDTE.TextDocument;
if(null == textDocument) {
return VSConstants.S_OK;
}
OptionPageConvLF optionPage = package_.Options;
OptionPageConvLF.TypeLineFeed linefeed = OptionPageConvLF.TypeLineFeed.LF;
if(null != optionPage) {
switch(document.Language) {
case "C/C++":
linefeed = optionPage.LineFeedCPP;
break;
case "CSharp":
linefeed = optionPage.LineFeedCSharp;
break;
default:
linefeed = optionPage.LineFeedOthers;
break;
}
}
# if DEBUG
Output(string.Format("Language {0}\n", document.Language));
# endif
string replaceLineFeed;
switch(linefeed) {
case OptionPageConvLF.TypeLineFeed.LF:
replaceLineFeed = "\n";
break;
case OptionPageConvLF.TypeLineFeed.CR:
replaceLineFeed = "\r";
break;
default:
replaceLineFeed = "\r\n";
break;
}
int count = 0;
EnvDTE.EditPoint editPoint = textDocument.StartPoint.CreateEditPoint();
if(OptionPageConvLF.TypeLineFeed.CRLF == linefeed) {
while(!editPoint.AtEndOfDocument) {
editPoint.EndOfLine();
string lineending = editPoint.GetText(1);
if(string.IsNullOrEmpty(lineending)) {
continue;
}
for(int i = 0; i < lineending.Length; ++i) {
switch(lineending[i]) {
case '\n':
editPoint.ReplaceText(1, replaceLineFeed, 0);
++count;
break;
case '\r':
if((lineending.Length - 1)<=i) {
editPoint.ReplaceText(1, replaceLineFeed, 0);
++count;
} else if('\n' != lineending[i+1]) {
editPoint.ReplaceText(1, replaceLineFeed, 0);
++count;
} else {
++i;
}
break;
default:
break;
}
}
editPoint.CharRight();
}
} else {
while(!editPoint.AtEndOfDocument) {
editPoint.EndOfLine();
string lineending = editPoint.GetText(1);
if(string.IsNullOrEmpty(lineending)) {
continue;
}
for(int i = 0; i < lineending.Length; ++i) {
if(lineending[i] != replaceLineFeed[0]) {
editPoint.ReplaceText(1, replaceLineFeed, 0);
++count;
}
}
editPoint.CharRight();
}
}
# if DEBUG
Output(string.Format("Replace {0} EOLs\n", count));
# endif
return VSConstants.S_OK;
}
まとめ
簡単なVisual Studio拡張をいちから作成しました.
完全に同じコードの拡張を以下に公開しています.
Market Place