13

Roslyn API を使用して基本的なコードの差分を作成しようとしていますが、予期しない問題が発生しています。基本的に、2 つのコードは同じですが、1 行追加されています。これは、変更されたテキストの行を返すだけのはずですが、何らかの理由で、すべてが変更されたことがわかります。行を追加するのではなく、1行だけ編集しようとしましたが、同じ結果が得られます。これをソース ファイルの 2 つのバージョンに適用して、2 つのバージョンの違いを識別できるようにしたいと考えています。現在使用しているコードは次のとおりです。

        SyntaxTree tree = SyntaxTree.ParseCompilationUnit(
            @"using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;

            namespace HelloWorld
            {
                class Program
                {
                    static void Main(string[] args)
                    {
                        Console.WriteLine(""Hello, World!"");
                    }
                }
            }");

        var root = (CompilationUnitSyntax)tree.Root;

        var compilation = Compilation.Create("HelloWorld")
                                     .AddReferences(
                                        new AssemblyFileReference(
                                            typeof(object).Assembly.Location))
                                     .AddSyntaxTrees(tree);

        var model = compilation.GetSemanticModel(tree);
        var nameInfo = model.GetSemanticInfo(root.Usings[0].Name);
        var systemSymbol = (NamespaceSymbol)nameInfo.Symbol;

        SyntaxTree tree2 = SyntaxTree.ParseCompilationUnit(
            @"using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;

            namespace HelloWorld
            {
                class Program
                {
                    static void Main(string[] args)
                    {
                        Console.WriteLine(""Hello, World!"");
                        Console.WriteLine(""jjfjjf"");
                    }
                }
            }");

        var root2 = (CompilationUnitSyntax)tree2.Root;

        var compilation2 = Compilation.Create("HelloWorld")
                                     .AddReferences(
                                        new AssemblyFileReference(
                                            typeof(object).Assembly.Location))
                                     .AddSyntaxTrees(tree2);

        var model2 = compilation2.GetSemanticModel(tree2);
        var nameInfo2 = model2.GetSemanticInfo(root2.Usings[0].Name);
        var systemSymbol2 = (NamespaceSymbol)nameInfo2.Symbol;

        foreach (TextSpan t in tree2.GetChangedSpans(tree))
        {
            Console.WriteLine(tree2.Text.GetText(t));
        }

そして、ここに私が得ている出力があります:

System
                using System
Collections
Generic
                using System
Linq
                using System
Text

                namespace HelloWorld
                {
                    class Program
                    {
                        static
Main
args
                        {
                            Console
WriteLine
"Hello, World!"
                            Console.WriteLine("jjfjjf");
                        }
                    }
                }
Press any key to continue . . .

興味深いことに、追加された行を除いて、各行がすべての行のトークンとして表示され、行を分割せずに表示されるようです。実際の変更を分離する方法を知っている人はいますか?

4

3 に答える 3

19

ブルース・ボートンの推測は正しい。GetChangedSpansメソッドは、履歴が共有されていない2つの構文ツリーを区別するための汎用の構文差分メカニズムを意図したものではありません。むしろ、編集によって生成された2つのツリーを共通のツリーに取り込み、編集によってツリーのどの部分が異なるかを判別することを目的としています。

最初の解析ツリーを取得し、編集として新しいステートメントを挿入した場合、変更のセットははるかに小さくなります。

Roslynレクサーとパーサーがどのように機能するかを高レベルで簡単に説明すると役立つ場合があります。

基本的な考え方は、レクサーで生成された「構文トークン」とパーサーで生成された「構文木」は不変であるということです。彼らは決して変わらない。それらは決して変更されないため、以前の解析ツリーの一部を新しい解析ツリーで再利用できます。(このプロパティを持つデータ構造は、「永続的な」データ構造と呼ばれることがよくあります。)

既存のパーツを再利用できるため、たとえば、classプログラムに表示される特定のトークンのすべてのインスタンスに同じ値を使用できます。すべてのトークンの長さと内容classはまったく同じです。class2つの異なるトークンを区別する唯一のものは、それらの雑学(それらを囲む間隔とコメント)とそれらの位置、およびそれらの(トークンを含むより大きな構文ノード)です。

テキストのブロックを解析すると、構文トークンと構文ツリーが永続的で不変の形式で生成されます。これを「グリーン」形式と呼びます。次に、緑のノードを「赤」のレイヤーにまとめます。緑のレイヤーは、位置や親などについて何も知りません。赤い層はそうです。(気まぐれな名前は、このデータ構造をホワイトボードに最初に描画したときに使用した色であるという事実によるものです。)特定の構文ツリーに編集を作成するときは、前の構文ツリーを見て、識別します。変更されたノード、および変更のスパインにのみ新しいノードを構築します。緑の木の他のすべての枝は同じままです。

2つのツリーを比較する場合、基本的には、緑のノードのセットの違いを取得します。一方のツリーがもう一方のツリーを編集して作成された場合、スパインのみが再構築されたため、ほとんどすべての緑色のノードが同じになります。ツリー差分アルゴリズムは、変更されたノードを識別し、影響を受けたスパンを計算します。

2つのツリーに共通の履歴がない場合、共通する緑色のノードは個々のトークンのみです。これは、前に述べたように、どこでも再利用されます。すべての上位レベルの緑の構文ノードは異なる緑のノードになるため、テキストが同じであっても、ツリーの階差機関によって異なるものとして扱われます。

このメソッドの目的は、編集後、元に戻すなど、テキストバッファのどの部分を再色付けする必要があるかについて、エディタコードが迅速に保守的な推測を行えるようにすることです。樹木には歴史的な関係があると想定されています。意図は、汎用のテキストの違いのメカニズムを提供することではありません。そのための優れたツールはすでにたくさんあります。

たとえば、最初のプログラムをエディターに貼り付け、次に全体を強調表示してから、2番目のプログラムをエディターに貼り付けたとします。貼り付けられたコードのどの部分が以前に貼り付けられたコードと同じであるかを編集者が理解しようとして時間を無駄にしないことが合理的に期待されます。それは非常に高額になる可能性があり、答えは「それほど多くない」可能性があります。むしろ、編集者は、貼り付けられた領域全体がまったく新しい、まったく異なるコードであるという控えめな仮定を立てています。古いコードと新しいコードを対応させるために時間を費やすことはありません。それは再解析し、したがって全体を再着色します。

一方、1つの異なるステートメントを貼り付けたばかりの場合、編集エンジンは編集を適切な場所に挿入するだけです。解析ツリーは、可能な場合は既存の緑色のノードを再利用して再生成され、階差機関は、再色付けする必要のあるスパン、つまり緑色のノードが異なるスパンを識別します。

それはすべて意味がありますか?

アップデート:

ハ、どうやらケビンと私は隣接するオフィスで同時に同じ答えを入力していたようです。少し重複した努力がありますが、どちらの答えも状況について良い見通しを持っていると思います。:-)

于 2011-11-29T16:53:15.957 に答える
13

@bruceboughton は正しく、GetChangedSpansインクリメンタル パーサーによって行われた変更を検出することを目的としています。次のようなコードを使用すると、はるかに優れた出力が得られます。

        var code = 
        @"using System; 
        using System.Collections.Generic; 
        using System.Linq; 
        using System.Text; 

        namespace HelloWorld 
        { 
            class Program 
            { 
                static void Main(string[] args) 
                { 
                    Console.WriteLine(""Hello, World!""); 
                } 
            } 
        }";
        var text = new StringText(code);
        SyntaxTree tree = SyntaxTree.ParseCompilationUnit(text);

        var index = code.IndexOf("}");
        var added = @"    Console.WriteLine(""jjfjjf""); 
                      ";

        var code2 = code.Substring(0, index) + 
                    added +
                    code.Substring(index);

        var text2 = new StringText(code2);

        var tree2 = tree.WithChange(text2, new [] { new TextChangeRange(new TextSpan(index, 0), added.Length) } );

        foreach (var span in tree2.GetChangedSpans(tree))
        {
            Console.WriteLine(text2.GetText(span));
        }

ただし、一般的に、GetChangedSpans は、高速ではあるが保守的な差分を提供することを目的としています。差分をより細かく制御し、より正確な結果を得るには、ニーズに合わせて調整できる独自のツリー差分アルゴリズムを実装することをお勧めします。

上記のコードでは、VS を使用している場合、エディターにはオブジェクトを簡単に構築できるようにする変更レポートとテキスト差分が組み込まれていTextChangeRangeますが、それ以外の場合は、少なくともテキスト差分アルゴリズムが必要になるでしょう。インクリメンタルパーサーに変更を渡すことができます。

于 2011-11-29T16:52:04.117 に答える