チュートリアル: 対応するかっこを表示する

かっこのマッチングなど、言語ベースの機能を実装します。それには、対応させたいかっこを定義し、かっこの一方にあるのがキャレットであるときには対応するかっこにテキスト マーカー タグを追加します。 言語のコンテキストでかっこを定義し、独自のファイル名拡張子とコンテンツ タイプを定義して、そのタイプだけにタグを適用するか、既存のコンテンツ タイプ ("text" など) にタグを適用することができます。 以下のチュートリアルでは、かっこ照合タグを "text" コンテンツ タイプに適用する方法を示します。

Managed Extensibility Framework (MEF) プロジェクトを作成する

MEF プロジェクトを作成するには

  1. エディター分類子プロジェクトを作成します。 ソリューション BraceMatchingTestの名前を指定します。

  2. プロジェクトに、[エディター分類子] 項目テンプレートを追加します。 詳細については、「エディター項目テンプレートを使用して拡張機能を作成する」を参照してください。

  3. 既存のクラス ファイルを削除します。

かっこ照合タガーを実装する

Visual Studio で使用されているものに似たかっこの強調表示効果を得る場合は、TextMarkerTag 型のタガーを実装できます。 以下のコードでは、どの入れ子レベルにあってもかっこのペアに対してタガーを定義する方法を示しています。 この例のタガー コンストラクターでは、[] と {} のかっこのペアが定義されていますが、完全な言語実装では、適切なかっこのペアが、言語仕様において定義されます。

かっこ照合タガーを実装するには

  1. クラス ファイルを追加し、その名前を BraceMatching にします。

  2. 次の名前空間をインポートします。

    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using Microsoft.VisualStudio.Text;
    using Microsoft.VisualStudio.Text.Editor;
    using Microsoft.VisualStudio.Text.Tagging;
    using Microsoft.VisualStudio.Utilities;
    
  3. TextMarkerTag 型の ITagger<T> を継承する BraceMatchingTagger クラスを定義します。

    internal class BraceMatchingTagger : ITagger<TextMarkerTag>
    
  4. テキスト ビュー、ソース バッファー、現在のスナップショット ポイントのプロパティに加えて、かっこのペアの組のためのプロパティも追加します。

    ITextView View { get; set; }
    ITextBuffer SourceBuffer { get; set; }
    SnapshotPoint? CurrentChar { get; set; }
    private Dictionary<char, char> m_braceList;
    
  5. タガー コンストラクター内で、プロパティを設定し、ビュー変更イベントの PositionChangedLayoutChanged をサブスクライブします。 この例では、説明目的で、対応するペアがコンストラクターでも定義されています。

    internal BraceMatchingTagger(ITextView view, ITextBuffer sourceBuffer)
    {
        //here the keys are the open braces, and the values are the close braces
        m_braceList = new Dictionary<char, char>();
        m_braceList.Add('{', '}');
        m_braceList.Add('[', ']');
        m_braceList.Add('(', ')');
        this.View = view;
        this.SourceBuffer = sourceBuffer;
        this.CurrentChar = null;
    
        this.View.Caret.PositionChanged += CaretPositionChanged;
        this.View.LayoutChanged += ViewLayoutChanged;
    }
    
  6. ITagger<T> 実装の一部として、TagsChanged イベントを宣言します。

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
    
  7. イベント ハンドラーでは、CurrentChar プロパティの現在のキャレット位置を更新し、TagsChanged イベントを発生させます。

    void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        if (e.NewSnapshot != e.OldSnapshot) //make sure that there has really been a change
        {
            UpdateAtCaretPosition(View.Caret.Position);
        }
    }
    
    void CaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
    {
        UpdateAtCaretPosition(e.NewPosition);
    }
    void UpdateAtCaretPosition(CaretPosition caretPosition)
    {
        CurrentChar = caretPosition.Point.GetPoint(SourceBuffer, caretPosition.Affinity);
    
        if (!CurrentChar.HasValue)
            return;
    
        var tempEvent = TagsChanged;
        if (tempEvent != null)
            tempEvent(this, new SnapshotSpanEventArgs(new SnapshotSpan(SourceBuffer.CurrentSnapshot, 0,
                SourceBuffer.CurrentSnapshot.Length)));
    }
    
  8. Visual Studio でのように、現在の文字が左かっこであるとき、または前の文字が右かっこであるときにかっこを照合する GetTags メソッドを実装します。 一致が検出されると、このメソッドは 2 つのタグをインスタンス化します。1 つは左かっこ用、もう 1 つは右かっこ用です。

    public IEnumerable<ITagSpan<TextMarkerTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        if (spans.Count == 0)   //there is no content in the buffer
            yield break;
    
        //don't do anything if the current SnapshotPoint is not initialized or at the end of the buffer
        if (!CurrentChar.HasValue || CurrentChar.Value.Position >= CurrentChar.Value.Snapshot.Length)
            yield break;
    
        //hold on to a snapshot of the current character
        SnapshotPoint currentChar = CurrentChar.Value;
    
        //if the requested snapshot isn't the same as the one the brace is on, translate our spans to the expected snapshot
        if (spans[0].Snapshot != currentChar.Snapshot)
        {
            currentChar = currentChar.TranslateTo(spans[0].Snapshot, PointTrackingMode.Positive);
        }
    
        //get the current char and the previous char
        char currentText = currentChar.GetChar();
        SnapshotPoint lastChar = currentChar == 0 ? currentChar : currentChar - 1; //if currentChar is 0 (beginning of buffer), don't move it back
        char lastText = lastChar.GetChar();
        SnapshotSpan pairSpan = new SnapshotSpan();
    
        if (m_braceList.ContainsKey(currentText))   //the key is the open brace
        {
            char closeChar;
            m_braceList.TryGetValue(currentText, out closeChar);
            if (BraceMatchingTagger.FindMatchingCloseChar(currentChar, currentText, closeChar, View.TextViewLines.Count, out pairSpan) == true)
            {
                yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(currentChar, 1), new TextMarkerTag("blue"));
                yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
            }
        }
        else if (m_braceList.ContainsValue(lastText))    //the value is the close brace, which is the *previous* character 
        {
            var open = from n in m_braceList
                       where n.Value.Equals(lastText)
                       select n.Key;
            if (BraceMatchingTagger.FindMatchingOpenChar(lastChar, (char)open.ElementAt<char>(0), lastText, View.TextViewLines.Count, out pairSpan) == true)
            {
                yield return new TagSpan<TextMarkerTag>(new SnapshotSpan(lastChar, 1), new TextMarkerTag("blue"));
                yield return new TagSpan<TextMarkerTag>(pairSpan, new TextMarkerTag("blue"));
            }
        }
    }
    
  9. 次のプライベート メソッドでは、どのレベルの入れ子でも対応するかっこを検索します。 最初のメソッドでは、左文字に対応する右文字を検索します。

    private static bool FindMatchingCloseChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
    {
        pairSpan = new SnapshotSpan(startPoint.Snapshot, 1, 1);
        ITextSnapshotLine line = startPoint.GetContainingLine();
        string lineText = line.GetText();
        int lineNumber = line.LineNumber;
        int offset = startPoint.Position - line.Start.Position + 1;
    
        int stopLineNumber = startPoint.Snapshot.LineCount - 1;
        if (maxLines > 0)
            stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);
    
        int openCount = 0;
        while (true)
        {
            //walk the entire line
            while (offset < line.Length)
            {
                char currentChar = lineText[offset];
                if (currentChar == close) //found the close character
                {
                    if (openCount > 0)
                    {
                        openCount--;
                    }
                    else    //found the matching close
                    {
                        pairSpan = new SnapshotSpan(startPoint.Snapshot, line.Start + offset, 1);
                        return true;
                    }
                }
                else if (currentChar == open) // this is another open
                {
                    openCount++;
                }
                offset++;
            }
    
            //move on to the next line
            if (++lineNumber > stopLineNumber)
                break;
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber);
            lineText = line.GetText();
            offset = 0;
        }
    
        return false;
    }
    
  10. 次のヘルパー メソッドでは、右文字に対応する左文字を検索します。

    private static bool FindMatchingOpenChar(SnapshotPoint startPoint, char open, char close, int maxLines, out SnapshotSpan pairSpan)
    {
        pairSpan = new SnapshotSpan(startPoint, startPoint);
    
        ITextSnapshotLine line = startPoint.GetContainingLine();
    
        int lineNumber = line.LineNumber;
        int offset = startPoint - line.Start - 1; //move the offset to the character before this one
    
        //if the offset is negative, move to the previous line
        if (offset < 0)
        {
            line = line.Snapshot.GetLineFromLineNumber(--lineNumber);
            offset = line.Length - 1;
        }
    
        string lineText = line.GetText();
    
        int stopLineNumber = 0;
        if (maxLines > 0)
            stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);
    
        int closeCount = 0;
    
        while (true)
        {
            // Walk the entire line
            while (offset >= 0)
            {
                char currentChar = lineText[offset];
    
                if (currentChar == open)
                {
                    if (closeCount > 0)
                    {
                        closeCount--;
                    }
                    else // We've found the open character
                    {
                        pairSpan = new SnapshotSpan(line.Start + offset, 1); //we just want the character itself
                        return true;
                    }
                }
                else if (currentChar == close)
                {
                    closeCount++;
                }
                offset--;
            }
    
            // Move to the previous line
            if (--lineNumber < stopLineNumber)
                break;
    
            line = line.Snapshot.GetLineFromLineNumber(lineNumber);
            lineText = line.GetText();
            offset = line.Length - 1;
        }
        return false;
    }
    

かっこ照合タガー プロバイダーを実装する

タガーを実装するのに加えて、タガー プロバイダーを実装してエクスポートする必要もあります。 この場合、プロバイダーのコンテンツ タイプは "text" です。 そのため、かっこ照合はすべての種類のテキスト ファイルに表示されますが、より完全な実装では、特定のコンテンツ タイプに対してのみ、かっこの照合が適用されます。

かっこ照合タガー プロバイダーを実装するには

  1. IViewTaggerProvider から継承するタガー プロバイダーを宣言して、その名前を BraceMatchingTaggerProvider とし、ContentTypeAttribute として "text"、TagTypeAttribute として TextMarkerTag を指定して、それをエクスポートします。

    [Export(typeof(IViewTaggerProvider))]
    [ContentType("text")]
    [TagType(typeof(TextMarkerTag))]
    internal class BraceMatchingTaggerProvider : IViewTaggerProvider
    
  2. BraceMatchingTagger をインスタンス化する CreateTagger メソッドを実装します。

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

コードのビルドとテスト

このコードをテストするには、BraceMatchingTest ソリューションをビルドし、それを実験用インスタンスで実行します。

BraceMatchingTest ソリューションをビルドしてテストするには

  1. ソリューションをビルドします。

  2. デバッガーでこのプロジェクトを実行すると、Visual Studio の 2 つ目のインスタンスが起動されます。

  3. テキスト ファイルを作成し、対応するかっこを含む何らかのテキストを入力します。

    hello {
    goodbye}
    
    {}
    
    {hello}
    
  4. 左かっこの前にキャレットを置くときには、そのかっこと、対応する右かっこの両方を強調表示する必要があります。 右かっこのすぐ後にカーソルを置くときには、そのかっこと、対応する左かっこの両方を強調表示する必要があります。