VisualStudio拡張機能メモ(更新中)

少しずつ更新して[Squirrel](http://squirrel-lang.org/)の言語拡張目指す。
[言語サービスとエディターの拡張機能の概要](https://msdn.microsoft.com/ja-jp/library/dd885122.aspx)

長くなり過ぎなのでまとめページは後程作ります！
# 一先ず進行中のプロジェクトアップしました！
**僕と契約してコミッターになってよ！**
https://github.com/LightWings-GameProject/VS.Squirrel

---
---
---

#チュートリアル: ファイル名拡張子へのコンテンツの種類のリンク
順次内容を読んでいき、[チュートリアル: ファイル名拡張子へのコンテンツの種類のリンク](https://msdn.microsoft.com/ja-jp/library/ee372313.aspx)のプロジェクトを作成して試してみることに。

拡張機能用のSDKが必要なので予めインストールが必要。

アセンブリ参照の追加([方法: 参照マネージャーを使用して参照を追加または削除する](https://docs.microsoft.com/ja-jp/visualstudio/ide/how-to-add-or-remove-references-by-using-the-reference-manager))をしないと
using System.ComponentModel.Composition;
などが使えない。少しハマった。

ビルドをしたが生成物の使い方がわからないので調査中。

#チュートリアル: 余白のグリフの作成
一先ず[チュートリアル: 余白のグリフの作成](https://msdn.microsoft.com/ja-jp/library/ee361745.aspx)に進む

またしても「System.Windows.Shapes」をusingする為のアセンブリが分からない。

ググると発見。コレは名前からは分からない。
[How do I include System.Windows.Shapes?](https://stackoverflow.com/questions/576622/how-do-i-include-system-windows-shapes)

using Microsoft.VisualStudio.Text.Formatting;は
Microsoft.VisualStudio.Text.UIに入っている。

この章で書いたコード

```C#:TodoGlyphFactory.cs
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Controls;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;

namespace Squirrel
{
	internal class TodoGlyphFactory : IGlyphFactory
	{
		const double m_glyphSize = 16.0;

		public UIElement GenerateGlyph(IWpfTextViewLine line, IGlyphTag tag)
		{
			// Ensure we can draw a glyph for this marker.
			if (tag == null || !(tag is TodoTag))
			{
				return null;
			}

			System.Windows.Shapes.Ellipse ellipse = new Ellipse();
			ellipse.Fill = Brushes.LightBlue;
			ellipse.StrokeThickness = 2;
			ellipse.Stroke = Brushes.DarkBlue;
			ellipse.Height = m_glyphSize;
			ellipse.Width = m_glyphSize;

			return ellipse;
		}
	}

	[Export(typeof(IGlyphFactoryProvider))]
	[Name("TodoGlyph")]
	[Order(After = "VsTextMarker")]
	[ContentType("code")]
	[TagType(typeof(TodoTag))]
	internal sealed class TodoGlyphFactoryProvider : IGlyphFactoryProvider
	{
		public IGlyphFactory GetGlyphFactory(IWpfTextView view, IWpfTextViewMargin margin)
		{
			return new TodoGlyphFactory();
		}
	}
}
```

```C#:TodoTagger.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

namespace Squirrel
{
	internal class TodoTag : IGlyphTag
	{
	}

	internal class TodoTagger : ITagger<TodoTag>
	{
		private IClassifier m_classifier;
		private const string m_searchText = "todo";

		public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

		internal TodoTagger(IClassifier classifier)
		{
			m_classifier = classifier;
		}

		IEnumerable<ITagSpan<TodoTag>> ITagger<TodoTag>.GetTags(NormalizedSnapshotSpanCollection spans)
		{
			foreach (SnapshotSpan span in spans)
			{
				//look at each classification span \
				foreach (ClassificationSpan classification in m_classifier.GetClassificationSpans(span))
				{
					//if the classification is a comment
					if (classification.ClassificationType.Classification.ToLower().Contains("comment"))
					{
						//if the word "todo" is in the comment,
						//create a new TodoTag TagSpan
						int index = classification.Span.GetText().ToLower().IndexOf(m_searchText);
						if (index != -1)
						{
							yield return new TagSpan<TodoTag>(new SnapshotSpan(classification.Span.Start + index, m_searchText.Length), new TodoTag());
						}
					}
				}
			}
		}
	}

	[Export(typeof(ITaggerProvider))]
	[ContentType("code")]
	[TagType(typeof(TodoTag))]
	class TodoTaggerProvider : ITaggerProvider
	{
		[Import]
		internal IClassifierAggregatorService AggregatorService;

		public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag
		{
			if (buffer == null)
			{
				throw new ArgumentNullException("buffer");
			}

			return new TodoTagger(AggregatorService.GetClassifier(buffer)) as ITagger<T>;
		}
	}
}

```

![無題.png](https://qiita-image-store.s3.amazonaws.com/0/175864/02fe1bae-ef53-b4de-aae2-f3f15e0e683f.png)

成果物は「bin」フォルダに作られるようです。見覚えのあるファイルが出来上がっています。
![無題.png](https://qiita-image-store.s3.amazonaws.com/0/175864/837408f9-c717-c3c3-e544-3bdd04afc154.png)


# チュートリアル: テキスト ビューをカスタマイズします。
[チュートリアル: テキスト ビューをカスタマイズします。](https://msdn.microsoft.com/ja-jp/library/ee818135.aspx)

ここまできたら特に引っ掛かるところは無く。インテリセンスまですっ飛ばしても良いかも。

```c#:ViewPropertyModifier.cs
using System;
using System.Collections;
using System.Windows;
using System.Windows.Media;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;

namespace Squirrel
{
	[Export(typeof(IWpfTextViewCreationListener))]
	[ContentType("text")]
	[TextViewRole(PredefinedTextViewRoles.Document)]
	internal class TestViewCreationListener : IWpfTextViewCreationListener
	{
		[Import]
		internal IEditorFormatMapService FormatMapService = null;

		public void TextViewCreated(IWpfTextView textView)
		{
			IEditorFormatMap formatMap = FormatMapService.GetEditorFormatMap(textView);

			ResourceDictionary regularCaretProperties = formatMap.GetProperties("Caret");
			ResourceDictionary overwriteCaretProperties = formatMap.GetProperties("Overwrite Caret");
			ResourceDictionary indicatorMargin = formatMap.GetProperties("Indicator Margin");
			ResourceDictionary visibleWhitespace = formatMap.GetProperties("Visible Whitespace");
			ResourceDictionary selectedText = formatMap.GetProperties("Selected Text");
			ResourceDictionary inactiveSelectedText = formatMap.GetProperties("Inactive Selected Text");

			formatMap.BeginBatchUpdate();

			regularCaretProperties[EditorFormatDefinition.ForegroundBrushId] = Brushes.Magenta;
			formatMap.SetProperties("Caret", regularCaretProperties);

			overwriteCaretProperties[EditorFormatDefinition.ForegroundBrushId] = Brushes.Turquoise;
			formatMap.SetProperties("Overwrite Caret", overwriteCaretProperties);

			indicatorMargin[EditorFormatDefinition.BackgroundColorId] = Colors.LightGreen;
			formatMap.SetProperties("Indicator Margin", indicatorMargin);

			visibleWhitespace[EditorFormatDefinition.ForegroundColorId] = Colors.Red;
			formatMap.SetProperties("Visible Whitespace", visibleWhitespace);

			selectedText[EditorFormatDefinition.BackgroundBrushId] = Brushes.LightPink;
			formatMap.SetProperties("Selected Text", selectedText);

			inactiveSelectedText[EditorFormatDefinition.BackgroundBrushId] = Brushes.DeepPink;
			formatMap.SetProperties("Inactive Selected Text", inactiveSelectedText);

			formatMap.EndBatchUpdate();
		}
	}
}
```
![無題.png](https://qiita-image-store.s3.amazonaws.com/0/175864/e279c245-89f8-a01e-f9eb-5181f4ce6d0e.png)

#チュートリアル: ビューの表示要素、コマンド、および設定 (列ガイド) を作成します。
こちらはスキップ

#チュートリアル: テキストの強調表示
[チュートリアル: テキストの強調表示](https://msdn.microsoft.com/ja-jp/library/dd885121.aspx)
こちらでキーワードの強調表示ができるかも知れない？

```c#:HighlightWordTag.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using System.Windows.Media;

namespace Squirrel
{
	internal class HighlightWordTag : TextMarkerTag
	{
		public HighlightWordTag() : base("MarkerFormatDefinition/HighlightWordFormatDefinition") { }
	}

	[Export(typeof(EditorFormatDefinition))]
	[Name("MarkerFormatDefinition/HighlightWordFormatDefinition")]
	[UserVisible(true)]
	internal class HighlightWordFormatDefinition : MarkerFormatDefinition
	{
		public HighlightWordFormatDefinition()
		{
			this.BackgroundColor = Colors.LightBlue;
			this.ForegroundColor = Colors.DarkBlue;
			this.DisplayName = "Highlight Word";
			this.ZOrder = 5;
		}
	}

	internal class HighlightWordTagger : ITagger<HighlightWordTag>
	{
		ITextView View { get; set; }
		ITextBuffer SourceBuffer { get; set; }
		ITextSearchService TextSearchService { get; set; }
		ITextStructureNavigator TextStructureNavigator { get; set; }
		NormalizedSnapshotSpanCollection WordSpans { get; set; }
		SnapshotSpan? CurrentWord { get; set; }
		SnapshotPoint RequestedPoint { get; set; }
		object updateLock = new object();

		public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

		public HighlightWordTagger(ITextView view, ITextBuffer sourceBuffer, ITextSearchService textSearchService, ITextStructureNavigator textStructureNavigator)
		{
			this.View = view;
			this.SourceBuffer = sourceBuffer;
			this.TextSearchService = textSearchService;
			this.TextStructureNavigator = textStructureNavigator;
			this.WordSpans = new NormalizedSnapshotSpanCollection();
			this.CurrentWord = null;
			this.View.Caret.PositionChanged += CaretPositionChanged;
			this.View.LayoutChanged += ViewLayoutChanged;
		}

		void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
		{
			// If a new snapshot wasn't generated, then skip this layout   
			if (e.NewSnapshot != e.OldSnapshot)
			{
				UpdateAtCaretPosition(View.Caret.Position);
			}
		}

		void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
		{
			UpdateAtCaretPosition(e.NewPosition);
		}

		void UpdateAtCaretPosition(CaretPosition caretPosition)
		{
			SnapshotPoint? point = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);

			if (!point.HasValue)
				return;

			// If the new caret position is still within the current word (and on the same snapshot), we don't need to check it   
			if (CurrentWord.HasValue
				&& CurrentWord.Value.Snapshot == View.TextSnapshot
				&& point.Value >= CurrentWord.Value.Start
				&& point.Value <= CurrentWord.Value.End)
			{
				return;
			}

			RequestedPoint = point.Value;
			UpdateWordAdornments();
		}

		void UpdateWordAdornments()
		{
			SnapshotPoint currentRequest = RequestedPoint;
			List<SnapshotSpan> wordSpans = new List<SnapshotSpan>();
			//Find all words in the buffer like the one the caret is on  
			TextExtent word = TextStructureNavigator.GetExtentOfWord(currentRequest);
			bool foundWord = true;
			//If we've selected something not worth highlighting, we might have missed a "word" by a little bit  
			if (!WordExtentIsValid(currentRequest, word))
			{
				//Before we retry, make sure it is worthwhile   
				if (word.Span.Start != currentRequest
					 || currentRequest == currentRequest.GetContainingLine().Start
					 || char.IsWhiteSpace((currentRequest - 1).GetChar()))
				{
					foundWord = false;
				}
				else
				{
					// Try again, one character previous.    
					//If the caret is at the end of a word, pick up the word.  
					word = TextStructureNavigator.GetExtentOfWord(currentRequest - 1);

					//If the word still isn't valid, we're done   
					if (!WordExtentIsValid(currentRequest, word))
						foundWord = false;
				}
			}

			if (!foundWord)
			{
				//If we couldn't find a word, clear out the existing markers  
				SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(), null);
				return;
			}

			SnapshotSpan currentWord = word.Span;
			//If this is the current word, and the caret moved within a word, we're done.   
			if (CurrentWord.HasValue && currentWord == CurrentWord)
				return;

			//Find the new spans  
			FindData findData = new FindData(currentWord.GetText(), currentWord.Snapshot);
			findData.FindOptions = FindOptions.WholeWord | FindOptions.MatchCase;

			wordSpans.AddRange(TextSearchService.FindAll(findData));

			//If another change hasn't happened, do a real update   
			if (currentRequest == RequestedPoint)
				SynchronousUpdate(currentRequest, new NormalizedSnapshotSpanCollection(wordSpans), currentWord);
		}

		static bool WordExtentIsValid(SnapshotPoint currentRequest, TextExtent word)
		{
			return word.IsSignificant
				&& currentRequest.Snapshot.GetText(word.Span).Any(c => char.IsLetter(c));
		}

		void SynchronousUpdate(SnapshotPoint currentRequest, NormalizedSnapshotSpanCollection newSpans, SnapshotSpan? newCurrentWord)
		{
			lock (updateLock)
			{
				if (currentRequest != RequestedPoint)
					return;

				WordSpans = newSpans;
				CurrentWord = newCurrentWord;

				var tempEvent = TagsChanged;
				if (tempEvent != null)
					tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0, SourceBuffer.CurrentSnapshot.Length)));
			}
		}

		public IEnumerable<ITagSpan<HighlightWordTag>> GetTags(NormalizedSnapshotSpanCollection spans)
		{
			if (CurrentWord == null)
				yield break;

			// Hold on to a "snapshot" of the word spans and current word, so that we maintain the same  
			// collection throughout  
			SnapshotSpan currentWord = CurrentWord.Value;
			NormalizedSnapshotSpanCollection wordSpans = WordSpans;

			if (spans.Count == 0 || wordSpans.Count == 0)
				yield break;

			// If the requested snapshot isn't the same as the one our words are on, translate our spans to the expected snapshot   
			if (spans[0].Snapshot != wordSpans[0].Snapshot)
			{
				wordSpans = new NormalizedSnapshotSpanCollection(
					wordSpans.Select(span => span.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive)));

				currentWord = currentWord.TranslateTo(spans[0].Snapshot, SpanTrackingMode.EdgeExclusive);
			}

			// First, yield back the word the cursor is under (if it overlaps)   
			// Note that we'll yield back the same word again in the wordspans collection;   
			// the duplication here is expected.   
			if (spans.OverlapsWith(new NormalizedSnapshotSpanCollection(currentWord)))
				yield return new TagSpan<HighlightWordTag>(currentWord, new HighlightWordTag());

			// Second, yield all the other words in the file   
			foreach (SnapshotSpan span in NormalizedSnapshotSpanCollection.Overlap(spans, wordSpans))
			{
				yield return new TagSpan<HighlightWordTag>(span, new HighlightWordTag());
			}
		}

		[Export(typeof(IViewTaggerProvider))]
		[ContentType("text")]
		[TagType(typeof(TextMarkerTag))]
		internal class HighlightWordTaggerProvider : IViewTaggerProvider
		{
			[Import]
			internal ITextSearchService TextSearchService { get; set; }

			[Import]
			internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelector { get; set; }

			public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
			{
				//provide highlighting only on the top buffer   
				if (textView.TextBuffer != buffer)
					return null;

				ITextStructureNavigator textStructureNavigator =
					TextStructureNavigatorSelector.GetTextStructureNavigator(buffer);

				return new HighlightWordTagger(textView, buffer, TextSearchService, textStructureNavigator) as ITagger<T>;
			}
		}
	}
}

```
コレは同じ単語が出てきたらハイライトするというもの。
同じローカル変数を強調表示しているのはこういう仕組みだったんですね。

![無題.png](https://qiita-image-store.s3.amazonaws.com/0/175864/82c68ca2-56f6-2281-35ca-558c02e7d445.png)

#一向に近づかないゴール

間が空いてしまいました。
ここまで来たんですが.nutは識別できないし、これまでのサンプルは全てのテキストファイルに適用してしまいます。

少し立ち戻って中身を詳しく見ていきます。

「ContentTypeDefinition」等で検索して居ると、英語版のチュートリアルページに当たりました。
そこでテンプレートの存在が明らかになりましたのでこちらを参考にClassifierについて詳しく見ていきます。
（ここまで来るのにGitHubでコード検索しまくったりしてました。）

[Creating an Extension with an Editor Item Template](https://docs.microsoft.com/en-us/visualstudio/extensibility/creating-an-extension-with-an-editor-item-template)

実はこのページ最初に見てたページにも[同じもの](https://msdn.microsoft.com/ja-jp/library/dd885492.aspx)が存在していました。テンプレを初めから使えばよかったですね。。

#テンプレを使う

ここでは「Editor Classifier」を使います。
ビルドすると全て紫色の素敵なエディタ画面が表示されます。

だが欲しいのは「コレジャナイ」

C#だろうが、C++だろうが、Squirrelだろうが全て紫色です。

せめてSquirrelだけに適用したい。
ここで最初の「ファイル名拡張子へのコンテンツの種類のリンク」です。

```c#:FileAndContentTypes.cs
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Classification;

using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Controls;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;

namespace Squirrel
{
	internal static class FileAndContentTypeDefinitions
	{
		[Export]
		[Name("Squirrel")]
		[BaseDefinition("code")]
		internal static ContentTypeDefinition squirrelContentTypeDefinition;

		[Export]
		[FileExtension(".nut")]
		[ContentType("Squirrel")]
		internal static FileExtensionToContentTypeDefinition squirrelFileExtensionDefinition;
	}
}
```

この土日で立ち戻った時に色々裏取りしてました。ContentTypeDefinition は読んで字のごとく「分類を定義するもの」です。
「Squirrel」という名前の「code」分類を基にした定義です[^1]。
[^1]: ここでの「code」だけでは言語解釈まではされません。

次に重要なのが「FileExtensionToContentTypeDefinition」です。
これが、ファイルの拡張子と分類を結びつけます。

つまり、**「.nut」のファイル拡張子のテキストは「Squirrel」という分類として解釈しますよ**という意味になります。

さて、先ほどのテンプレと組み合わせます。
ここは超簡単です。

テンプレで作成した「○○Provider」クラスの「ContentType」属性に「"Squirrel"」を入れるだけです。

すると、「.nut」ファイルだけが全て紫色の素敵なエディタ画面が表示されます。

だが欲しいのは「コレジャナイ」

#また行き詰る

この後も悩みまして、ぶっちゃけ「パーフェクトなプロジェクト」が無いと無理なんじゃないかと思い始めました。

そこで今度はQiita内を検索しました。
先人が1年前に書いたコードを発見しましたので紹介します。

[(途中)Visual Studio SDKで新しい言語をサポートするためのメモ](https://qiita.com/quartorz/items/b4ea1ba03ad9356686b1)

こちらもメモ書きのようです。
しかしこの中にパーフェクトなプロジェクトの紹介がありましたのでリンクを張ります。
コレはPythonの言語拡張をMS様が書かれたもののようです。

[Python Tools for Visual Studio - Home](https://pytools.codeplex.com/)

はい。こちらはレガシーコードなのでダウンロードしてもVS2017では解釈不能です。

This project has moved and is read-only. For the latest updates, please go here.

と書かれているのでGihHubに飛びます。

[Microsoft/PTVS](https://github.com/Microsoft/PTVS)

はい、こちらが言語拡張の最新パーフェクトなプロジェクト。
正直これほど壮大なコードをちょっと楽をする為のツールとして実装するのは時間をかけすぎですね。。

#当面の目標はSyntax Highlightとします。

先ほどの[メモ](https://qiita.com/quartorz/items/b4ea1ba03ad9356686b1)を参考に読み進めると「IClassifierProvider」を使って各種の分類に振り分ければ良いらしいぞという情報を入手。

しかし、findで検索してクラス振り分けしてとなると**それだけでも結構骨が折れます**。
元のコードを参照すると、「分類器サポーター」的な実装がある事を知りました。


```c#:PTVS-master\Python\Product\PythonTools\PythonTools\PythonClassifier.cs
var bi = _provider.Services.GetBufferInfo(snapshot.TextBuffer);
```

この「bi」すごく便利そうですね。「PythonTextBufferInfo型」となっております。
という事はこれがイイカンジにパースしてくれてるんだろうと踏みました。

飛んで飛んでいくと「PythonTextBufferInfo::PythonTextBufferInfo(PythonEditorServices services, ITextBuffer buffer)」に辿り着きます。

注目すべき点はココ。

```c#:PTVS-master\Python\Product\PythonTools\PythonTools\Editor\PythonTextBufferInfo.cs
Buffer.ContentTypeChanged += Buffer_ContentTypeChanged;
```

見覚えのあるSyntaxですね。~~きっとMS様はこの書き方が大好きなのです。割込処理はWindowsだろうがXboxだろうがC#だろうがCLIだろうが「+=」でコールバックを代入するのです。~~ eventの実装です。

という事は、**どんなイベントが存在するのか分かれば**実装可能です。
早速「ITextBuffer」のドキュメントに行きます。

（続く）