コード補完はどのように機能しますか?


84

多くのエディターとIDEにはコード補完があります。それらのいくつかは非常に「インテリジェント」であり、他は実際にはそうではありません。よりインテリジェントなタイプに興味があります。たとえば、a)現在のスコープで使用可能であるb)戻り値が有効である場合にのみ関数を提供するIDEを見てきました。(たとえば、「5 + foo [tab]」の後では、正しいタイプの整数または変数名に追加できるものを返す関数のみが提供されます。)また、より頻繁に使用されるオプションまたは最長のオプションが優先されることも確認しました。リストの。

コードを解析する必要があることを理解しています。ただし、通常、現在のコードの編集は無効ですが、構文エラーがあります。不完全でエラーが含まれているものをどのように解析しますか?

時間の制約もあります。リストを作成するのに数秒かかる場合、完了は役に立ちません。完了アルゴリズムが数千のクラスを処理する場合があります。

これに適したアルゴリズムとデータ構造は何ですか?


1
良い質問です。codeblocks.orgのCode :: Blocksなど、これを実装するいくつかのオープンソースIDEのコードを確認することをお勧めします 。

1
ここではC#でコード補完を作成するには、記事のC#でコード補完を作成するには
プリタムZopeの

回答:


64

UnrealScript言語サービス製品のIntelliSenseエンジンは複雑ですが、ここではできる限り概要を説明します。VS2008 SP1のC#言語サービスが私のパフォーマンス目標です(正当な理由があります)。まだありませんが、ctrl + spaceやユーザーが.(ドット)を入力するのを待たずに、1文字を入力した後、安全に提案を提供できるほど高速で正確です。[言語サービスに取り組んでいる]人々がこの主題についてより多くの情報を得るほど、私が彼らの製品を使用した場合に、より良いエンドユーザーエクスペリエンスを得ることができます。不幸なことに、細部にまで気を配らなかった製品がたくさんあります。その結果、コーディングよりもIDEと戦っていました。

私の言語サービスでは、次のようにレイアウトされています。

  1. カーソルで式を取得します。これは、メンバーアクセス式の先頭からカーソルが置かれている識別子の末尾まで進みます。メンバーアクセス式は通常、の形式aa.bb.ccですが、のようにメソッド呼び出しを含めることもできますaa.bb(3+2).cc
  2. カーソルを取り巻くコンテキストを取得します。これは、コンパイラと同じルールに従うとは限らないため(長い話)、非常に注意が必要ですが、ここでは、従うと仮定します。通常、これは、カーソルが含まれるメソッド/クラスに関するキャッシュ情報を取得することを意味します。
  3. コンテキストオブジェクトがを実装するIDeclarationProviderとします。ここで呼び出すGetDeclarations()IEnumerable<IDeclaration>、スコープ内に表示されているすべてのアイテムを取得できます。私の場合、このリストには、ローカル/パラメーター(メソッド内の場合)、メンバー(フィールドとメソッド、インスタンスメソッド内でない限り静的、基本タイプのプライベートメンバーなし)、グローバル(言語Iのタイプと定数)が含まれます。に取り組んでいます)、およびキーワード。このリストには、という名前のアイテムが含まれますaa。#1で式を評価するための最初のステップとして、我々は、名前のコンテキストの列挙から項目を選択しaa、私たちに与えて、IDeclaration次のステップのために。
  4. 次に、演算子をIDeclaration表現に適用して、の「メンバー」(ある意味で)を含むaa別の演算子を取得します。以来、オペレータが異なるオペレータ、私が呼び出しと期待していたオブジェクトが正しく記載されている演算子を適用します。IEnumerable<IDeclaration>aa.->declaration.GetMembers(".")IDeclaration
  5. これは、を押すまで続きますcc。ここで、宣言リストに、という名前のオブジェクトが含まれる場合と含まれない場合がありますcc。ご存知のとおり、複数の項目がで始まる場合はcc、それらも表示されるはずです。私はこれを解決するために、最終的な列挙を取得し、それを文書化されたアルゴリズムに渡して、ユーザーに可能な限り最も役立つ情報を提供します。

IntelliSenseバックエンドに関する追加の注意事項は次のとおりです。

  • の実装では、LINQの遅延評価メカニズムを多用しますGetMembers。キャッシュ内の各オブジェクトは、そのメンバーを評価するファンクターを提供できるため、ツリーで複雑なアクションを実行するのは簡単です。
  • 各オブジェクトList<IDeclaration>がそのメンバーを保持する代わりに、を保持しますList<Name>。ここで、Nameは、メンバーを説明する特別にフォーマットされた文字列のハッシュを含む構造体です。名前をオブジェクトにマップする巨大なキャッシュがあります。このようにして、ファイルを再解析するときに、ファイルで宣言されているすべてのアイテムをキャッシュから削除し、更新されたメンバーを再入力できます。ファンクターの構成方法により、すべての式はすぐに新しいアイテムに評価されます。

IntelliSense「フロントエンド」

ユーザーが入力すると、ファイルは正しいよりも構文的に正しくないことがよくあります。そのため、ユーザーが入力したときにキャッシュのセクションを無計画に削除したくありません。増分更新を可能な限り迅速に処理するために、多数の特殊なケースのルールを用意しています。インクリメンタルキャッシュは開いているファイルに対してのみローカルに保持され、入力によってバックエンドキャッシュがファイル内の各メソッドなどの誤った行/列情報を保持していることにユーザーが気付かないようにするのに役立ちます。

  • 償還要因の1つは、パーサーが高速であることです。優先度の低いバックグラウンドスレッドで自己完結型で動作しながら、150ミリ秒で20000ラインソースファイルのフルキャッシュアップデートを処理できます。このパーサーが開いているファイルのパスを(構文的に)正常に完了すると、ファイルの現在の状態がグローバルキャッシュに移動されます。
  • ファイルが構文的に正しくない場合は、ANTLRフィルターパーサーを使用して(リンクについては申し訳ありません。ほとんどの情報はメーリングリストにあるか、ソースの読み取りから収集されます、ファイルを再解析して次のものを探します。
    • 変数/フィールド宣言。
    • クラス/構造体定義のシグネチャ。
    • メソッド定義のシグネチャ。
  • ローカルキャッシュでは、クラス/構造体/メソッドの定義は署名から始まり、中括弧のネストレベルが偶数に戻ったときに終了します。別のメソッド宣言に達した場合(ネストされたメソッドなし)、メソッドを終了することもできます。
  • ローカルキャッシュでは、変数/フィールドは直前の閉じられていない要素にリンクされます。これが重要である理由の例については、以下の簡単なコードスニペットを参照してください。
  • また、ユーザーが入力すると、追加/削除された文字範囲をマークする再マップテーブルを保持します。これは次の目的で使用されます。
    • メソッドは完全な解析間でファイル内を移動できる/移動しないため、カーソルの正しいコンテキストを識別できることを確認します。
    • Go To Declaration / Definition / Referenceが、開いているファイル内のアイテムを正しく検索していることを確認します。

前のセクションのコードスニペット:

class A
{
    int x; // linked to A

    void foo() // linked to A
    {
        int local; // linked to foo()

    // foo() ends here because bar() is starting
    void bar() // linked to A
    {
        int local2; // linked to bar()
    }

    int y; // linked again to A

このレイアウトで実装したIntelliSense機能のリストを追加すると思いました。それぞれの写真はここにあります。

  • オートコンプリート
  • ツールチップ
  • 方法のヒント
  • クラスビュー
  • コード定義ウィンドウ
  • ブラウザを呼び出す(VS 2010はついにこれをC#に追加します)
  • 意味的に正しいすべての参照を検索

これは素晴らしいありがとうです。並べ替えるときに大文字と小文字を区別するバイアスについて考えたことはありません。中括弧の不一致に対処できることが特に気に入っています。
stribika 2009

15

特定の実装でどのアルゴリズムが使用されているかを正確に言うことはできませんが、知識に基づいて推測することはできます。トライは、この問題のために非常に有用なデータ構造である:IDEは、各ノードでいくつかの余分なメタデータを、プロジェクト内のシンボルの全てのメモリに大規模なトライを維持することができます。

キャラクターを入力すると、トライ内のパスを歩きます。特定のトライノードのすべての子孫は、可能な補完です。IDEは、現在のコンテキストで意味のあるものでそれらを除外する必要がありますが、タブ補完ポップアップウィンドウに表示できる数だけ計算する必要があります。

より高度なタブ補完には、より複雑なトライが必要です。たとえば、Visual Assist Xには、キャメルケース記号の大文字を入力するだけでよい機能があります。たとえば、SFNと入力するSomeFunctionNameと、タブ補完ウィンドウに記号が表示されます。

トライ(または他のデータ構造)を計算するには、プロジェクト内のすべてのシンボルのリストを取得するために、すべてのコードを解析する必要があります。Visual Studioは.ncb、これをプロジェクトと一緒に保存されているファイルであるIntelliSenseデータベースに保存するため、プロジェクトを閉じて再度開くたびにすべてを再解析する必要はありません。大規模なプロジェクト(たとえば、フォームソース管理を同期したばかりのプロジェクト)を初めて開くと、VSはすべてを解析してデータベースを生成するのに時間がかかります。

増分変更をどのように処理するのかわかりません。あなたが言ったように、あなたがコードを書いているとき、それは90%の確率で無効な構文であり、アイドル状態になるたびにすべてを再解析することは、特にによって含まれるヘッダーファイルを変更している場合、CPUに大きな負担をかけます。多数のソースファイル。

(a)プロジェクトを実際にビルドするとき(またはプロジェクトを閉じたり開いたりしたとき)にのみ再解析するか、(b)ある種のローカル解析を実行して、現在の場所のコードのみを解析するのではないかと思います。関連するシンボルの名前を取得するために、いくつかの限定された方法で編集されました。C ++は非常に複雑な文法を持っているため、重いテンプレートメタプログラミングなどを使用している場合、暗い場所で奇妙に動作する可能性があります。


トライは本当に良い考えです。増分変更に関しては、それが機能しない場合は最初にファイルの再解析を試みて現在の行を無視し、それが機能しない場合は囲んでいる{...}ブロックを無視することができる場合があります。他のすべてが失敗した場合は、最後のデータベースを使用してください。
stribika 2009

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.