前回はエスケープシーケンスを使ってターミナルのサイズを取得する部分やその方法を紹介した。ソースコードはkilo / kilo.cで閲覧できるので参考にしてもらえればと思う。コメント抜きで1,000行以内なので実用的なCのコードを勉強するには扱いやすい教材だ。今回はKiloで実装されているシンタックスハイライトについて取り上げる。

処理を最初から見ていこう。まず、520行目辺りから始まっているeditorSelectSyntaxHighlight()という関数の中で、どのハイライトを利用するかを調べている。判断の基準はファイル名の拡張子だ。530行目辺りにある「strstr(filename,s->filematch[i])」の処理で拡張子が.cかまたは.cppだった場合に条件に一致するようになっており、今のところこの2つの拡張子にだけ対応している。

どのシンタックスハイライトを使用するか判定しているeditorSelectSyntaxHighlight()関数

/* Select the syntax highlight scheme depending on the filename,

 * setting it in the global state E.syntax. */
void editorSelectSyntaxHighlight(char *filename) {
    for (unsigned int j = 0; j < HLDB_ENTRIES; j++) {
        struct editorSyntax *s = HLDB+j;
        unsigned int i = 0;
        while(s->filematch[i]) {
            char *p;
            int patlen = strlen(s->filematch[i]);
            if ((p = strstr(filename,s->filematch[i])) != NULL) {
                if (s->filematch[i][0] != '.' || p[patlen] == '\0') {
                    E.syntax = s;
                    return;
                }
            }
            i++;
        }
    }
}

ここでタブを閉じようとしているあなた、ちょっと待ってほしい! そう、一見すると上のソースコードは複雑に見えるかもしれない。でも、やっていることはファイル名の拡張子を調べて、一致したらシンタックスハイライトの対象として設定しているだけだ。もうちょっと読んでほしい。

拡張子の定義は160行目辺りに書いてある。.Cや.cxxといった別の拡張子でも一致するようにしたいなら、ここに定義を追加すればよい。

拡張子の定義

char *C_HL_extensions[] = {".c",".cpp",NULL};

実際にKiloのシンタックスハイライトがどのように機能するのかを、kilo.cを編集している画面から見てみよう。次のように数字やコメント、キーワードに色がついていることを確認できる。

Kiloでkilo.cを編集しているところ - シンタックスハイライトが有効になっている

構文解析を行ってどの色を付けるかについては、370行目辺りから書かれているeditorUpdateSyntax()関数に書いてある。この関数は1行ごとに引数に行データを与えられた状態で呼び出される。行の先頭から対象の文字をどの色として描画すべきかを調べて、同じ長さの配列に1文字ごとに色データを書き込んでいく。

例えば、対象の行が「char multilinecommentstart[3];」なら、解析後のデータには「55550000000000000000000000000700」のようなデータが書き込まれている。シンタックスハイライトの色は55行目辺りから次のように定義されている。

ハイライトカラーの定義

/* Syntax highlight types */

define HL_NORMAL 0

define HL_NONPRINT 1

define HL_COMMENT 2   /* Single line comment. */

define HL_MLCOMMENT 3 /* Multi-line comment. */

define HL_KEYWORD1 4

define HL_KEYWORD2 5

define HL_STRING 6

define HL_NUMBER 7

define HL_MATCH 8      /* Search match. */

定義を見れば0が通常の文字、5がキーワード2、7が数字として分類されていることがわかる。対応する実際の色を表現するコードは505行目辺りから定義されているeditorSyntaxToColor()という関数で定義されている。

505行目辺りに定義されているeditorSyntaxToColor()関数

/* Maps syntax highlight token types to terminal colors. */

int editorSyntaxToColor(int hl) {
    switch(hl) {
    case HL_COMMENT:
    case HL_MLCOMMENT: return 36;     /* cyan */
    case HL_KEYWORD1: return 33;    /* yellow */
    case HL_KEYWORD2: return 32;    /* green */
    case HL_STRING: return 35;      /* magenta */
    case HL_NUMBER: return 31;      /* red */
    case HL_MATCH: return 34;      /* blu */
    default: return 37;             /* white */
    }
}

つまり、あとは出力する文字列データを作成する段階で、上記関数を利用して色データを取り出し、シンタックスデータに対応して実際に色を変更するエスケープシーケンスを挿入していけば、結果出力される画面にはシンタックスハイライト済みの文字列が表示されることになる。この処理は860行目辺りから始まるeditorRefreshScreen()関数の次の処理を見るとわかりやすい。「snprintf(buf,sizeof(buf),"\x1b[%dm",color);」の処理で色を変えるエスケープシーケンスを挿入している。

エスケープシーケンスで色を変えるデータを挿入しているところ

                    int color = editorSyntaxToColor(hl[j]);
                if (color != current_color) {
                    char buf[16];
                    int clen = snprintf(buf,sizeof(buf),"\x1b[%dm",color);
                    current_color = color;
                    abAppend(&ab,buf,clen);
                }

こうして作成されたデータは970行目辺りの「write(STDOUT_FILENO,ab.b,ab.len);」という処理で一気にターミナルに出力されている。シンタックスハイライトを行うために文字の色を変えるエスケープシーケンスを含んだ文字列データを1画面分作成して、それを最後に出力しているというわけだ。

シンタックスハイライトの部分はファイルデータをメモリ上に展開したあとのデータに対し、「構文解析(的なもの)」「ハイライトのためのデータ生成」「エスケープシーケンスデータの挿入」という処理が行われている。

最初から全部追っていくのはC言語に慣れていないとちょっとばかり大変かもしれない。しかし、基本的にやっていることは調べて文字の色を変えるエスケープシーケンスを挿入しているだけだ。実のところ、それほど複雑な処理はおこなっていない。

シンタックスハイライトの処理は拡張可能な作りになっているので、例えば拡張子が.javaだったらJava的なシンタックスハイライトを行うようにするとか、.pyだった時はPython的なシンタックスハイライトを行うようにするといったことが簡単にできる。

とりあえずCのソースコードに慣れてみたいなら、この辺りの機能追加を行ってMyKiloを作ってみてはいかがだろうか。それほど苦労せずに成果が得られると思うので、面白い課題だと思う。