(2018/4/1追記)
これは2015年頃にVisual Studio Haskellを作ろうとしていたときに書いたメモです。
(追記ここまで)
Visual Studio SDKについて分かったことを書いています。
「新しいプロジェクト」ダイアログに新しいテンプレートを追加するまではここを参考にしました。
それ以降はPython ToolsのソースコードとMSDNを参考にしています。
Visual Studio 2013を使っています。
作り始めるまで
- Visual Studio SDKをインストールする
- Visual Studioを起動し、「新しいプロジェクト」ダイアログを開いてVisual C#→拡張機能→Visual Studio Packageと選んでプロジェクトを作る(以下プロジェクトの名前を「プロジェクト名」と書く)
- ウィザードのPage 3「Select Package Options」のチェックを3つとも入れておく
- Page 6でソースコードの拡張子を決められる(以下ここで決めた拡張子を「独自の拡張子」と書く)
- MPFをダウンロードする
- zipファイルを解凍して
Dev12\Src\CSharp
にあるプロジェクトを丸ごと2.で作ったソリューションに追加して出力パスを..\プロジェクト名\bin\DebugまたはRelease\
に変える- もしくはソリューションに追加せずにプロジェクトをビルドして、
Microsoft.VisualStudio.Project.dll
を2.で作ったプロジェクトの参照に追加する
- もしくはソリューションに追加せずにプロジェクトをビルドして、
- Python Toolsの
Common\Product\SharedProject
を丸ごと2.で作ったプロジェクトに追加する - Python Toolsをビルドして
Microsoft.VisualStudio.ReplWindow.dll
を拾ってきて参照を追加する。
参照を追加するのはプロジェクトファイルをテキストエディタで開いて適当な場所に
<Reference Include="Microsoft.VisualStudio.ReplWindow">
<HintPath>.\Microsoft.VisualStudio.ReplWindow.dll</HintPath>
</Reference>
のように書けばできる。
MPFはMicrosoft Public LicenseでSharedProjectはApache License 2.0。
デバッグが開始できないときはプロジェクトのプロパティの「デバッグ」を開いて「外部プログラムの開始」にC:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe
、「コマンドライン引数」に/rootsuffix Exp
と書けば開始できる。
プロジェクトを作れるようにする
「新しいプロジェクト」ダイアログからプロジェクトを作れるようにする。
大体ここの通りやってうまくいったので注意点や気づいた所だけ。
- C#のプロジェクトを追加する場合以外は
AssemblyInfo.cs
はコピーしなくていい -
Templates\Projects\SimpleProject
に入れたファイルのプロパティのビルドアクションをコンテンツ、Include in VSIXをTrue
にするのを忘れないようにする- 忘れても特にエラーとかは出ないけど忘れてると「新しいプロジェクト」ダイアログに追加されない
- 「You must change part of the code in the Visual Studio MPF for Projects code.」って書いてあるけど修正しなくていい(たぶん今ダウンロードできるのは修正済みのやつ)
- プロジェクトファイルはページ内で示されてるXMLと似た形式にしないとプロジェクトを作る時にエラーになる
-
Microsoft.VisualStudio.Project.ProjectPackage
の代わりにMicrosoft.VisualStudioTools.CommonPackage
を使うとコマンドを追加したりREPL用のウィンドウを作ったりするのが楽になりそう
Microsoft.VisualStudioTools.CommonPackage
を使うなら、ProjectFactory
はMicrosoft.VisualStudioTools.Project.ProjectFactory
、ProjectNode
はMicrosoft.VisualStudioTools.Project.CommonProjectNode
を継承するように変更する必要がある。
自分の場合はSharedProject\ProjectResources.cs
の_manager
の定義も以下のように変更する必要があった。
private static readonly Lazy<ResourceManager> _manager = new Lazy<ResourceManager>(
() => new ResourceManager("Microsoft.VisualStudioTools.Project.SR", typeof(SR).Assembly),
LazyThreadSafetyMode.ExecutionAndPublication
);
以下ではMicrosoft.VisualStudioTools.CommonPackage
を使っているものとして説明を書く。
メニュー
項目の追加
プロジェクト名.vsct
のMyMenuとかを見てそれっぽく
パッケージクラスがMicrosoft.VisualStudioTools.CommonPackage
を継承してるなら、Microsoft.VisualStudioTools.Command
を継承したクラスのインスタンスをRegisterCommands
に渡すだけでコマンドの実行が実装できる。
Python\Product\PythonTools\PythonTools\Commands
内のプログラムが参考になりそう。
オプション
エディタ設定の追加
言語情報クラス
-
Guids.cs
にguidプロジェクト名LanguageService
みたいな名前のGUIDを追加する- 自分は他の項目に合わせて
string
型の方にLanguageServiceString
、Guid型の方にLanguageService
って付けたけどPython ToolsではguidPythonLanguageService
とguidPythonLanguageServiceGuid
になってた
- 自分は他の項目に合わせて
-
LanguageInfo
クラスを追加する-
IVsLanguageInfo
インタフェースを実装する -
Python\Product\PythonTools\Navigation\PythonLanguageInfo.cs
を参考にする
-
サービスクラス
Python\Product\PythonTools\PythonToolsService.cs
を参考にService
クラスを作る。
とりあえずコンストラクタのLanguageInfo
クラスが関わってる所だけ実装する。
言語設定クラス
Python\Product\PythonTools\PythonTools\Editor\LanguagePreferences.cs
を参考にLanguagePreferences
クラスを作る。
各メソッドはとりあえず全部空にしといていいと思う。
オプションサービスクラス
Python\Product\PythonTools\PythonTools\Options\PythonToolsOptionsService.cs
を参考にIOptionsService
インタフェースとOptionsService
クラスを作る。
パッケージクラスに追加
Python\Product\PythonTools\PythonToolsPackage.cs
を参考にする。
まず属性にProvideLanguageService
とProvideLanguageExtension
を追加する。
Constants.FileExtension
とかは適宜作成する。
次にInitialize
メソッドのPythonToolsOptionsService
とかをAddService
してる部分を参考にOptionsService
とService
をAddService
する。
最後に、OptionsService
のコンストラクタでプロジェクト名Package.GetSettings
が呼ばれてるはずなので、
internal static SettingsManager GetSettings(System.IServiceProvider serviceProvider)
{
return new ShellSettingsManager(serviceProvider);
}
を追加する。
Python ToolsだとここでSettingsManagerCreator.GetSettingsManager
を呼び出してるけど今はこれで動くと思う。
ここまでするとオプションダイアログ内の「テキスト エディター」以下に独自の項目が増えるはず。
項目を増やす
ProvideLanguageEditorOptionPage
を使えば良さそう(未確認)
エディタ設定の変更に対応する
調べてない。
「テキスト エディター」じゃないところに項目を作る
Microsoft.PythonTools.Options.PythonGeneralOptionsPage
とか参考にして何とかOptionsPage
を作る。
パッケージクラスにProvideOptionPage
属性をつける。
「テキスト エディター」じゃないところの項目の変更に対応する
調べてない。
エディタ設定との関連も不明。
エディタ
Windows Formsを使わないようにする
自動生成されたコードでは独自の拡張子のファイルを表示にするためにRichTextBox
を使っているが、Python ToolsではVisual Studio SDKのAPIだけを使って表示している。
RichTextBox
を使っていると以下の作業がうまくいかない(ClassifierProvider
が作られない)ので、Python Toolsと同じようにプログラムを変更する。
Python\Product\PythonTools\PythonTools\Project\PythonEditorFactory.cs
を参考にMicrosoft.VisualStudioTools.Project.CommonEditorFactory
を継承してInitializeLanguageService
をオーバーライドすればいい。
シンタックスハイライト
作業の大まかな流れ
文字列をあらかじめ用意しておいたタイプに分類することで色を付ける。
やってないならここにある通りにsource.extension.vsixmanifestのAssetsにMEF Componentを追加する必要がある。
- コンテンツタイプを作る
- 分類タイプを用意する
-
IClassifierProvider
を実装したクラスを作ってそのクラスとコンテンツタイプを関連付ける - 3.で作ったClassifierProviderの
GetClassifier(ITextBuffer buffer)
メソッドで独自のClassifierを返す- Python Toolsだとここで、作ったClassifierを
buffer.Property
に追加してる
- Python Toolsだとここで、作ったClassifierを
- ファイルが編集されるたびに
Classifier.GetClassificationSpans
が呼ばれるので、ここで分類して色を付ける
コンテンツタイプ
[Export]
[Name("適当な名前")]
[BaseDefinition("code")]
internal static ContentTypeDefinition MyContentTypeDefinition;
このようなメンバを適当なクラスに宣言すればコンテンツタイプが作れる。
コンテンツタイプの一覧は
foreach (var c in ((IComponentModel)Microsoft.VisualStudio.Shell.Package.GetGlobalService(typeof(SComponentModel))).GetService<IContentTypeRegistryService>().ContentTypes)
{
Trace.WriteLine(c.DisplayName);
}
みたいにすれば確認できるので、これでコンテンツタイプが作られてるか分かる。
分類タイプ
Python\Product\PythonTools\Resources.resx
や
Python\Product\PythonTools\PythonTools\Project\ProjectResources.cs
を見るとClassificationType
で終わるリソース名やメンバ変数がいくつかある。
まずこれらとPython\Product\PythonTools\PythonTools\PythonClassifierProvider.cs
を参考にしてResourceManager
からリソースを取得する処理を書く。
[Export]
[Name("名前")]
[BaseDefinition(PredefinedClassificationTypeNames.Identifier)]
internal static ClassificationTypeDefinition IdentifierClassificationDefinition = null;
このようにClassificationTypeDefinition
を宣言すると分類タイプを定義できる。
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = "名前")]
[Name("名前")]
[UserVisible(true)]
[Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)]
internal sealed class IdentifierFormat : ClassificationFormatDefinition
{
public IdentifierFormat()
{
DisplayName = ProjectResources.GetString(ProjectResources.IdentifierClassificationType);
ForegroundColor = Colors.Blue;
}
}
こうすると上で定義したタイプに対応した書式を作ることができる。
実行して「ツール→オプション→環境→フォントおよび色」の「表示項目」に何とかFormat.DisplayName
と同じ名前の項目が増えていれば上手くいっている。
IClassifierProvider
Python\Product\PythonTools\PythonTools\PythonClassifierProvider.csを参考に実装する。
[Export(typeof(IClassifierProvider)), ContentType("コンテンツタイプ")]
internal class ClassifierProvider : IClassifierProvider {
こうすることでコンテンツタイプが一致するファイルが開かれたときにClassifierProviderが作られてGetClassifier
メソッドが呼ばれるようになる。
分類器
ClassifierProvider.GetClassifier
で分類器を返す。
ファイルに変更があると分類器のGetClassificationSpans
メソッドが呼ばれるので、構文解析などしてIList<ClassificationSpan>
を作って返せばハイライトされる。
REPL用のウィンドウ
Python Toolsの実装
メニューに「Python バージョン Interactive
」みたいな名前の項目がインストールされてるPythonの数だけ表示されている。
これは.vsctにあらかじめ16個メニューが用意してあって、インストール済みのPythonの数だけ表示させて残りの項目を隠す事で実現している。
各メニューをクリックしたときの動作はOpenReplCommand
クラスに定義されている。
BasePythonReplEvaluator
のExecute
から始まる名前のメソッドが呼ばれる時にEnsureConnected
が呼ばれて、その中でvisualstudio_py_repl.py
が実行されるようになってる。
BasePythonReplEvaluator.CommandProcessorThread
がこのプロセスを監視する。
とりあえず表示させる
コマンド
何も実行できないけどとりあえずそれっぽいウィンドウを表示させる。
まずPython\Product\PythonTools\PythonTools\Commands\OpenReplCommand.cs
のDoCommand
を見ると、
var window = (ToolWindowPane)ExecuteInReplCommand.EnsureReplWindow(_serviceProvider, factory, null);
このような行があるので、Python\Product\PythonTools\PythonTools\Commands\ExecuteInReplCommand.csのEnsureReplWindowを見に行くと、
window = provider.CreateReplWindow(
serviceProvider.GetPythonContentType(),
factory.Description + " Interactive",
typeof(PythonLanguageInfo).GUID,
replId
);
このような部分が見つかる。
とりあえずOpenReplCommand.DoCommand
とExecuteInReplCommand.EnsureReplWindow
をこのCreateReplWindow
を呼び出すように実装する。
ExecuteInReplCommand.EnsureReplWindow
に渡してるfactory
はとりあえず代わりにnull
を渡しておけばいいと思う。
LibraryManager
CreateReplWindow
が呼ばれるとパッケージクラスのLibraryManager CreateLibraryManager(CommonPackage package)
が呼ばれる。
ここでnull
を返すと例外が投げられるのでPython\Product\PythonTools\PythonTools\Navigation\PythonLibraryManager.cs
を参考に適当にLibraryManager
を作っておいてこれのインスタンスを返すようにする。
ReplEvaluator
IReplEvaluator
を実装したReplEvaluator
クラスを作る。
各メソッドはとりあえず何もせずに適当な値を返すようにする。
ReplEvaluatorProvider
Python\Product\PythonTools\PythonTools\Repl\PythonReplEvaluatorProvider.cs
を参考にReplEvaluatorProvider
を作る。
ReplEvaluatorProvider
に[Export(typeof(IReplEvaluatorProvider))]
という属性を付けるとIReplWindowProvider.CreateReplWindow
が呼ばれた時にGetEvaluator
が呼ばれるようになる。
IReplEvaluator GetEvaluator(string replId)
で自分で作ったReplEvaluator
を返すようにする。
ここまで作ってメニューのツールから独自の項目を選ぶと、REPL用のウィンドウが生成されるはず。
対話実行
Python Toolsと同じようにEnsureConnected
を呼び出すようにする。
stdoutとstderrをリダイレクトさせて読めばとりあえずそれっぽいものはできる。
プロジェクト
修正
ProjectNodeProperties
ソリューションエクスプローラーで項目を右クリックすると例外が発生するので修正する。
最初はCommonProjectNodeProperties.OutputType
のget
でNotImplementedException
が投げられているはずなのでそれを修正する。
ProjectNode
でCommonProjectNode.CreatePropertiesObject
メソッドをオーバーライドして独自のProjectNodeProperties
クラスのインスタンスを作って返すようにする。
ProjectNodeProperties
クラスは、Microsoft.VisualStudioTools.Project.CommonProjectNodeProperties
と同じように作ってOutput
はVSLangProj.prjOutputType.prjOutputTypeExe
とか適当な値を返すようにしておく。
ClipboardService
パッケージクラスのInitialize
メソッドにservices.AddService(typeof(IClipboardService), new ClipboardService(), promote: true);
を追加する。
プロジェクトの設定
Microsoft.PythonTools.Project.PythonDebugPropertyPage
とかを参考にしてクラスを作る。
Control
プロパティでWindows Formsで作ったコントロールを返す。
Guid
属性を付けておく。
ProjectNode
でCommonProjectNode.GetConfigurationIndependentPropertyPages
をオーバーライドしてさっき作ったPropertyPage
のGUIDを返す。
Configuration
はDebugとかReleaseとかのことっぽい?
パッケージクラスに[ProvideObject(typeof(Project.PropertyPage))]
属性を付ける。