テキスト列番号の単位は何ですか?

最近、構文解析コンビネータ ライブラリ lexy を公開しました。 入力が文法と一致しない場合。このエラーには 15 があります エラーが発生した位置がわかります。

ハッピー パスを高速に保つには、22 はエンド ユーザーにとって使いやすいものではありません。入力範囲への単純な反復子です。これは、問題のある入力を簡単に見つけるために行番号や列番号などを求める人間のユーザーには適していません。

イテレータを行/列の位置に変換するのは簡単に思えます:set 32 イテレータの位置に到達するまで、入力全体を繰り返します。改行が表示されるたびに、行番号を増やし、列番号を 49 に戻します。 .それ以外の場合、列は毎回実装されます…正確には何を参照してください?

テキストの「列」とは正確には何ですか?どのように計算するのですか?

アプローチ #1:56 を数える s

問題をあまり考えずに、基本的なバージョンを書きましょう:

template <typename Input, typename Iterator>
auto find_location(const Input& input, Iterator position)
{
    auto line   = 1;
    auto column = 1;

    for (auto iter = input.begin(); iter != input.end(); ++iter)
    {
        if (iter == position)
        {
            // We found the location.
            break;
        }
        else if (*iter == '\n') // End of a line.
        {
            ++line;
            column = 1;
        }
        else
        {
            // Advance one column.
            ++column;
        }
    }

    return std::make_pair(line, column);
}

改行に遭遇すると、次の行に進みます。それ以外の場合は、列をインクリメントします。探している入力の位置に到達したら、ループを終了して結果を返します。

これは機能し、非常にシンプルで直感的です。いくつかのテキスト エディタとコンパイラをテストしましたが、このアルゴリズムは、clang、バージョン 11 より前の GCC、および neovims 62 で使用されているようです。 関数。

しかし、このアルゴリズムは「間違っています」。

76 の数を数えています は、Unicode の世界では「文字」の概念とは関係のない行にあります。88 のように入力してください 、 91 、または 108 UTF-8 ではそれぞれ 2、3、4 列をカウントしますが、UTF-16 では 1、1、2 をカウントします。

ですから、もっとうまくやる必要があります。

アプローチ #2:コード ポイントをカウントする

説明のために、入力が UTF-8 でエンコードされていると仮定しましょう。UTF-8 はマルチバイト エンコーディングです。これは、一部の「文字」が 111 のシーケンスを使用してエンコードされることを意味します。 .単一の 128 コードユニットと呼ばれます コードポイントをエンコードするために一連のコード単位が使用されます .139 のような「文字」 、 144 、または 152 1 つのコード ポイントですが、複数のコード単位としてエンコードされます。

したがって、161 ではなく、コード ポイントを数える必要があります。 s:

for (auto iter = input.begin(); iter != input.end(); )
{
    if (iter == position)
    {
        // We found the location.
        break;
    }
    else if (*iter == '\n') // End of a line.
    {
        ++line;
        column = 1;
    }
    else
    {
        // One code point is a column.
        skip_code_point(iter, input.end());
        ++column;
    }
}

関数 170 反復子を次のコード ポイントに進めるために必要なロジックを実行します。これはそれほど複雑ではありません。最初のコード ユニットのビット パターンを見てください。簡潔にするためにここでは省略しました。

コード ポイントをカウントするということは、マルチバイトの「文字」でさえ 1 つの列として扱われ、実際のエンコーディングを公開しないことを意味します。このアルゴリズムは Rust コンパイラで使用されているようです。

そのため、列のカウントは最初に予想したよりも少し複雑ですが、それでも問題はありません.lexy は既に Unicode コード ポイントに一致するルールを提供しているので、実際の実装でそれらを使用して、1 日で終わりましょう.

ただし、それほど単純ではありません。

テキストの処理はそれほど単純ではありません。

アプローチ #3:書記素クラスターを数える

「文字」を引用符で囲んでいることに注目してください。

これは、「文字」が実際にはコード単位やコード ポイントのような正確な定義を持っていないためです。非技術者が文字として説明するものに最も近いのは、Unicode 書記素クラスタ です。 :フォント内の単一のグリフにほぼ対応する文字列。

そしてもちろん、1 つの書記素クラスターをエンコードするには、1 つのコード ポイントでは十分ではなく、複数必要になる場合があります。多くのラテン文字を特別なコード ポイントと組み合わせて、f̃、w͜、s̷̙̃ などの文字を形成できます。これらは 2、3 です。 、それぞれ 4 つのコード ポイントがあります。ハングルやタイ語など、レンダリング時に複数のコード ポイントを組み合わせて使用​​するスクリプトもあり、絵文字もあります。

絵文字は、多くのコード ポイントを 1 つの記号に簡単に組み合わせます。🇪🇺 などの旗の絵文字で始まり、実際には特別な「E」の後に「U」が続き、🧑‍🔬 (科学者) などの絵文字が続きます。 (人) は特別な建具コード ポイントを使用して 🔬 (顕微鏡) と一緒に接着され、コード ポイントの組み合わせの絶対的な頂点である家族の絵文字 👪 で終わります。トーンと性別修飾子) を作成し、別の人やその子供に貼り付けます。そうすれば、10 個以上のコード ポイントで構成される 1 つの「文字」を簡単に作成できます!

したがって、「文字」を適切に数えるには、コード ポイントではなく、書記素クラスタ全体で位置を進める必要があります。これは、LibreOffice などの「実際の」テキスト プログラムが行うことです。

これは確かに実行可能ですが、複雑に思えます (そして、絵文字シーケンスをカバーするかどうかさえわかりません…?) 実装する前に、これが実際に私たちが望むアプローチであることを確認しましょう.

アプローチ #4:仮想列を数える

エラーを報告するとき、コンパイラは入力の関連部分にも下線を付けます:

error: this is not how things work!
  my_really_cool_program(42);
                         ^^ this is wrong

そのためには、下線を印刷する前に印刷するスペースの数を知る必要があります。列をその数のスペースとして定義すると、これは 仮想列 とも呼ばれます。 .neovims 184 によって報告されています 関数であり、バージョン 11 以降の GCC で使用されています (GNU 標準で推奨されているようです)。

同等のスペースの数を数えることは、フォントによって異なるため、一般的に簡単ではありません。ただし、ここでは、すべてのグリフが同じ幅 (mono) を持つモノスペース フォントを安全に想定できます。 スペースですよね?)

もちろんそうではありません。

ほとんどの中国語、日本語、または韓国語の文字は、等幅フォントであっても、他のほとんどの文字の 2 倍の幅でレンダリングされます:

1234 // 4 characters
全角 // 2 characters

また、193 などの通常の文字のワイド バージョンもあります。 (207 ではありません ).しかし、Unicode 標準とルックアップ テーブルがあるので、それは悪くないようです.

ただし、絵文字は 2 倍の幅でレンダリングされます:

12
🙂

そして 212 があります 、タブ文字です。あえて聞いてみましょう:タブは何個のスペースですか?

なんらかの理由で、GCC は「8」と表示しているようです。 は 4 つのスペースとして表示されますが、下線は 8 つを想定しています。

GCC と neovim の間の非互換性はそれだけにとどまりません:複数のコード ポイントから一緒に貼り付けられた絵文字を覚えていますか?

もちろん、neovim はそれらを適切にレンダリングしません。 、ここで 247 これは、neovim によると、🧑‍🔬仮想列の長さが 259 であることを意味します。 、したがって、下線でそれを説明するには10個のスペースが必要です。ただし、GCCは4個のスペースのみを出力します(絵文字ごとに2個、目に見えないグルーコードポイントには0個)。 P>

そして、あなたは本当にそれを責めることができますか?

私の「実際の」端末では、🧑‍🔬は 260 としてレンダリングされます 、したがって、4 つのスペースを印刷することは正しいです (これは私の端末が適切にレンダリングしないためでもありますが、それは 2 つになります)。したがって、「この文字の幅はいくつのスペースですか?」に答えるには、環境に問い合わせる必要があります。 /font を使用しています – モノスペース フォントでも!

言うまでもなく、このアプローチも正しくないようです。

次は?

要約すると、4 つのアプローチを見てきました。

  • コード単位のカウント:計算は簡単で高速ですが、「文字」とは実際には関係がないため、ユーザーは驚くかもしれません。
  • コード ポイントのカウント:バイト数のカウントよりも複雑で「より正確」ですが、それでも「文字」との実際の関係はありません。
  • 書記素クラスタのカウント:さらに複雑ですが、少なくとも「文字」に対応します。
  • 仮想列のカウント:なんとなくさらに複雑ですが、少なくともエラー メッセージに下線を引くことができます。

どうすればいいですか?

その答えを得るには、一歩下がって理由を実際に確認する必要があります そもそも列情報が必要です。特に、エディターとコンパイラーという 2 つの異なるユース ケースがあります。

エディターの場合、ユーザーにカーソル位置を知らせる列を表示します。そこでは、書記素クラスターを数えることが正しいアプローチだと思います。これには、列が「を押す必要がある頻度に直接対応する」という利点があります272 カーソルの移動も書記素クラスターに基づいているため、ユーザーに「5 番目の位置にいます」と伝えることは、「矢印キーを 5 回押してそこに移動する」ことを意味します。 .

コンパイラの場合、ユーザーがエラーの位置を特定できるように列を表示します。ユーザーが出力を見て、手動でそのエラーの場所に移動した場合、これは書記素クラスターの数でもあるはずです。これは矢印の動きに対応するためです。 .

しかし、誰もエラー メッセージを見て、列の情報を使用してその場所に手動で移動することはありません!IDE/vim のセットアップは自動的にエラーの場所にジャンプします (または、下線を見て、列をまったく見ずに手動でそこに移動します)。

つまり、エラーの場所は、計算しやすい単位 (コード単位) で、IDE で簡単に解析できる形式で記述する必要があります。 /P>

これを、GCC が使用しようとしている仮想列と比較してください:適切に計算するには、環境に依存します!特に、neovim と GCC の定義は一致しません。これは、エラーの場所への自動ジャンプが不可能であることを意味します.GNU の使用の決定将来のデフォルトの仮想列は見当違いのようです.

誤解しないでください。仮想列にはその場所があります。しかし、それでも、計算するのはまったく自明ではありません:🧑‍🔬 の正しい値を 2 と報告しますか?それとも、ほとんどの端末とバグ互換で 4 と言うでしょうか?どちらの場合でも、そうではありません。まだ異なってレンダリングされているため、neovim 内では機能しません。どこでも機能する正解がないタブは言うまでもありません。

機械が解析できるべきものに、明確な定義のないこのような脆弱なユニットを使用することは、単にトラブルを求めているだけです.neovimがそれを列の位置として使用することを選択する理由は理解できます.実際の列に非常に似ているものです.しかし、私はこれでさえユーザーにとって有用だとは思わない:位置を示すのに相当するスペースの数を知る必要があるのはなぜですか?

これにより、中間のコードポイントが残ります:計算が複雑で、ユーザーにとってはあまり役に立ちません.ただし、コード単位とは異なり、それらは実際のエンコーディングとは無関係です.したがって、UTF-16 の入力ファイルがあり、コンパイラが UTF-コード ポイントで位置を指定すると、コンパイラとエディタで同じ結果が得られます。

これが発生する 1 つのシナリオは、言語サーバーを使用する場合です。入力ファイルは通常 UTF-8 ですが、言語サーバー プロトコルは UTF-16 を想定しています。列情報をコード ポイントで示すのが理想的ですが、UTF-16 コード単位を使用します。代わりに、サーバーがトランスコードする必要があります。代わりにコード ポイントを使用するには、移植可能であるため、未解決の問題があることに注意してください。

結論

1 つの表の要約:

カウンティング マシン 人間 ポータブル
コード単位 簡単 役に立たない いいえ
コード ポイント 中程度 役に立たない はい
書記素クラスタ 難しい 役に立つ はい
仮想列 難しい あまり役に立ちませんか? 絶対にありません

そのため、場所がマシンによって解析されることを意図している場合 (コンパイラ エラー メッセージなど) は単位としてコード単位を使用し、場所が人間にとって有用であることを意図している場合 (テキスト エディターなど) は単位として書記素クラスターを使用します。

異なるエンコーディング間で通信する必要がある場合は、コード単位よりもコード ポイントを使用してください。

仮想列は、それが実際に必要な場合にのみ使用してください (複数の行を揃える場合など)。エラー メッセージなどのポータブルな出力形式として使用すると、問題が発生するだけです。

lexy では、単位はカスタマイズ可能であり、実際にカスタマイズ可能です。 ただし、この問題についてはドキュメントでもう少し詳しく説明します。