Edited at

(途中)Visual Studio SDKで新しい言語をサポートするためのメモ

More than 1 year has passed since last update.

(2018/4/1追記)

これは2015年頃にVisual Studio Haskellを作ろうとしていたときに書いたメモです。

(追記ここまで)

Visual Studio SDKについて分かったことを書いています。

「新しいプロジェクト」ダイアログに新しいテンプレートを追加するまではここを参考にしました。

それ以降はPython ToolsのソースコードとMSDNを参考にしています。

Visual Studio 2013を使っています。


作り始めるまで


  1. Visual Studio SDKをインストールする

  2. Visual Studioを起動し、「新しいプロジェクト」ダイアログを開いてVisual C#→拡張機能→Visual Studio Packageと選んでプロジェクトを作る(以下プロジェクトの名前を「プロジェクト名」と書く)


    • ウィザードのPage 3「Select Package Options」のチェックを3つとも入れておく

    • Page 6でソースコードの拡張子を決められる(以下ここで決めた拡張子を「独自の拡張子」と書く)




  3. MPFをダウンロードする

  4. zipファイルを解凍してDev12\Src\CSharpにあるプロジェクトを丸ごと2.で作ったソリューションに追加して出力パスを..\プロジェクト名\bin\DebugまたはRelease\に変える


    • もしくはソリューションに追加せずにプロジェクトをビルドして、Microsoft.VisualStudio.Project.dllを2.で作ったプロジェクトの参照に追加する



  5. Python ToolsのCommon\Product\SharedProjectを丸ごと2.で作ったプロジェクトに追加する

  6. 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を使うなら、ProjectFactoryMicrosoft.VisualStudioTools.Project.ProjectFactoryProjectNodeMicrosoft.VisualStudioTools.Project.CommonProjectNodeを継承するように変更する必要がある。

自分の場合はSharedProject\ProjectResources.cs_managerの定義も以下のように変更する必要があった。


SharedProject\ProjectResources.cs

        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内のプログラムが参考になりそう。


オプション


エディタ設定の追加


言語情報クラス



  1. Guids.csguidプロジェクト名LanguageServiceみたいな名前のGUIDを追加する


    • 自分は他の項目に合わせてstring型の方にLanguageServiceString、Guid型の方にLanguageServiceって付けたけどPython ToolsではguidPythonLanguageServiceguidPythonLanguageServiceGuidになってた




  2. 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を参考にする。

まず属性にProvideLanguageServiceProvideLanguageExtensionを追加する。

Constants.FileExtensionとかは適宜作成する。

次にInitializeメソッドのPythonToolsOptionsServiceとかをAddServiceしてる部分を参考にOptionsServiceServiceAddServiceする。

最後に、OptionsServiceのコンストラクタでプロジェクト名Package.GetSettingsが呼ばれてるはずなので、


プロジェクト名Package.cs

        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を追加する必要がある。


  1. コンテンツタイプを作る

  2. 分類タイプを用意する


  3. IClassifierProviderを実装したクラスを作ってそのクラスとコンテンツタイプを関連付ける

  4. 3.で作ったClassifierProviderのGetClassifier(ITextBuffer buffer)メソッドで独自のClassifierを返す


    • Python Toolsだとここで、作ったClassifierをbuffer.Propertyに追加してる



  5. ファイルが編集されるたびにClassifier.GetClassificationSpansが呼ばれるので、ここで分類して色を付ける


コンテンツタイプ

        [Export]

[Name("適当な名前")]
[BaseDefinition("code")]
internal static ContentTypeDefinition MyContentTypeDefinition;

このようなメンバを適当なクラスに宣言すればコンテンツタイプが作れる。

コンテンツタイプの一覧は


プロジェクト名Package.cs

            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を参考に実装する。


ClassifierProvider

    [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クラスに定義されている。

BasePythonReplEvaluatorExecuteから始まる名前のメソッドが呼ばれる時にEnsureConnectedが呼ばれて、その中でvisualstudio_py_repl.pyが実行されるようになってる。

BasePythonReplEvaluator.CommandProcessorThreadがこのプロセスを監視する。


とりあえず表示させる


コマンド

何も実行できないけどとりあえずそれっぽいウィンドウを表示させる。

まずPython\Product\PythonTools\PythonTools\Commands\OpenReplCommand.csDoCommandを見ると、


OpenReplCommand.cs

            var window = (ToolWindowPane)ExecuteInReplCommand.EnsureReplWindow(_serviceProvider, factory, null);


このような行があるので、Python\Product\PythonTools\PythonTools\Commands\ExecuteInReplCommand.csのEnsureReplWindowを見に行くと、


ExecuteInReplCommand.cs

                window = provider.CreateReplWindow(

serviceProvider.GetPythonContentType(),
factory.Description + " Interactive",
typeof(PythonLanguageInfo).GUID,
replId
);

このような部分が見つかる。

とりあえずOpenReplCommand.DoCommandExecuteInReplCommand.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.OutputTypegetNotImplementedExceptionが投げられているはずなのでそれを修正する。

ProjectNodeCommonProjectNode.CreatePropertiesObjectメソッドをオーバーライドして独自のProjectNodePropertiesクラスのインスタンスを作って返すようにする。

ProjectNodePropertiesクラスは、Microsoft.VisualStudioTools.Project.CommonProjectNodePropertiesと同じように作ってOutputVSLangProj.prjOutputType.prjOutputTypeExeとか適当な値を返すようにしておく。


ClipboardService

パッケージクラスのInitializeメソッドにservices.AddService(typeof(IClipboardService), new ClipboardService(), promote: true);を追加する。


プロジェクトの設定

Microsoft.PythonTools.Project.PythonDebugPropertyPageとかを参考にしてクラスを作る。

ControlプロパティでWindows Formsで作ったコントロールを返す。

Guid属性を付けておく。

ProjectNodeCommonProjectNode.GetConfigurationIndependentPropertyPagesをオーバーライドしてさっき作ったPropertyPageのGUIDを返す。

ConfigurationはDebugとかReleaseとかのことっぽい?

パッケージクラスに[ProvideObject(typeof(Project.PropertyPage))]属性を付ける。