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

(2018/4/1追記)
これは2015年頃に[Visual Studio Haskell](https://github.com/quartorz/VisualStudioHaskell)を作ろうとしていたときに書いたメモです。
(追記ここまで)

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

「新しいプロジェクト」ダイアログに新しいテンプレートを追加するまでは[ここ](https://msdn.microsoft.com/en-us/library/cc512961.aspx)を参考にしました。
それ以降は[Python Tools](https://pytools.codeplex.com/)のソースコードと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](https://mpfproj12.codeplex.com/)をダウンロードする
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`を拾ってきて参照を追加する。

参照を追加するのはプロジェクトファイルをテキストエディタで開いて適当な場所に

```xml
    <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`と書けば開始できる。

# プロジェクトを作れるようにする
「新しいプロジェクト」ダイアログからプロジェクトを作れるようにする。
大体[ここ](https://msdn.microsoft.com/en-us/library/cc512961.aspx)の通りやってうまくいったので注意点や気づいた所だけ。

* 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`の定義も以下のように変更する必要があった。

```csharp: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.cs`に`guidプロジェクト名LanguageService`みたいな名前のGUIDを追加する
    * 自分は他の項目に合わせて`string`型の方に`LanguageServiceString`、Guid型の方に`LanguageService`って付けたけどPython Toolsでは`guidPythonLanguageService`と`guidPythonLanguageServiceGuid`になってた
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`を参考にする。

まず属性に`ProvideLanguageService`と`ProvideLanguageExtension`を追加する。
`Constants.FileExtension`とかは適宜作成する。

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

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

```csharp:プロジェクト名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`をオーバーライドすればいい。

## シンタックスハイライト
### 作業の大まかな流れ
文字列をあらかじめ用意しておいたタイプに分類することで色を付ける。

やってないなら[ここ](https://msdn.microsoft.com/en-us/library/ee372313.aspx)にある通りに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`が呼ばれるので、ここで分類して色を付ける

### コンテンツタイプ

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

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

```csharp:プロジェクト名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`からリソースを取得する処理を書く。

```csharp
        [Export]
        [Name("名前")]
        [BaseDefinition(PredefinedClassificationTypeNames.Identifier)]
        internal static ClassificationTypeDefinition IdentifierClassificationDefinition = null;
```

このように`ClassificationTypeDefinition`を宣言すると分類タイプを定義できる。

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

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

`BasePythonReplEvaluator`の`Execute`から始まる名前のメソッドが呼ばれる時に`EnsureConnected`が呼ばれて、その中で`visualstudio_py_repl.py`が実行されるようになってる。
`BasePythonReplEvaluator.CommandProcessorThread`がこのプロセスを監視する。

## とりあえず表示させる
### コマンド
何も実行できないけどとりあえずそれっぽいウィンドウを表示させる。
まず`Python\Product\PythonTools\PythonTools\Commands\OpenReplCommand.cs`の`DoCommand`を見ると、

```csharp:OpenReplCommand.cs
            var window = (ToolWindowPane)ExecuteInReplCommand.EnsureReplWindow(_serviceProvider, factory, null);
```

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

```csharp:ExecuteInReplCommand.cs
                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))]`属性を付ける。