dlangui で SyntaxSupport を使ったシンタックスハイライト実装ハマりポイントまとめ
dlangui の SourceEdit に libdparse を使ったシンタックスハイライトを実装する際に踏んだハマりポイントをまとめます。
環境
- dlangui 0.10.8
- libdparse 0.25.1
- DMD (x86_64 / Windows)
ハマりポイント 1:load() の前に syntaxSupport をセットしても無効
症状
auto syntax = new DSyntaxSupport();
ed.content.syntaxSupport = syntax; // ← セット
ed.load(path); // ← この後ハイライトが効かない
updateHighlight はログ上では呼ばれているのに色が変わらない。
原因
load() の内部実装を追うと、ファイル読み込み後に notifyContentReplaced() が呼ばれる。
この関数は handleContentChange() を呼ぶが、ReplaceContent アクションのとき rangeAfter が空になるため updateTokenProps(0, 0) となり、全行のトークンプロパティがリセットされたままになる。
// editable.d 内
void notifyContentReplaced() {
clearEditMarks();
handleContentChange(...); // rangeAfter が空 → updateTokenProps(0, 0)
}
解決策
load() の後に syntaxSupport をセットする。syntaxSupport のセッターは内部で updateTokenProps(0, lines.length) を呼ぶので全行が正しくハイライトされる。
ed.load(path); // 先にロード
if (detectFileType(path) == FileType.D) {
ed.content.syntaxSupport = new DSyntaxSupport(); // load() の後にセット
ed.setTokenHightlightColor(TokenCategory.Keyword, 0x00FB4934);
// ...
}
ハマりポイント 2:libdparse のトークンで text が空になるものがある
症状
updateHighlight でトークンを処理する際、キーワード(import など)の tok.text が空文字列になっており、文字ループが回らずに props への書き込みがスキップされる。
tok type=130 line=0 col=0 text= ← textが空!
原因
libdparse は静的トークン(キーワード・演算子)の text フィールドを省略することがある。トークンの文字列は str(tok.type) で取得する必要がある。
解決策
// tok.text が空の場合は str() でトークン文字列を取得
string rawText = tok.text.length > 0 ? tok.text : str(tok.type);
if (rawText.length == 0) continue;
dstring dtxt = toUTF32(rawText);
ハマりポイント 3:dlangui の色フォーマットはアルファが逆
症状
setTokenHightlightColor に 0xFFRRGGBB 形式で色を渡しても、全文字が transparent! と判定されてデフォルトのテキスト色に上書きされる。
transparent! i=0 color=4294658356 ← 0xFFBB3931、不透明なはずが透明扱い
原因
dlangui の isFullyTransparentColor の実装が一般的な慣習と逆になっている。
// dlangui/graphics/colors.d
/// returns true if color is #FFxxxxxx (color alpha is 255)
bool isFullyTransparentColor(uint color) pure nothrow {
return (color >> 24) == 0xFF;
}
通常の 0xAARRGGBB フォーマットでは AA=0xFF が不透明だが、dlangui では AA=0xFF が完全透明、AA=0x00 が不透明という逆の定義になっている。CoolReader(dlangui の前身 C++ ライブラリ)から引き継いだ独自仕様と思われる。
解決策
色定数の先頭バイト(アルファ部分)を 0xFF ではなく 0x00 にする。
// NG:先頭バイト 0xFF → dlangui では完全透明扱い
enum uint COLOR_KEYWORD = 0xFF_FB_49_34;
// OK:先頭バイト 0x00 → dlangui では不透明
enum uint COLOR_KEYWORD = 0x00_FB_49_34;
enum uint COLOR_STRING = 0x00_B8_BB_26;
enum uint COLOR_NUMBER = 0x00_D3_86_9B;
enum uint COLOR_COMMENT = 0x00_92_83_74;
enum uint COLOR_FG = 0x00_EB_DB_B2;
ハマりポイント 4:isKeyword() は使えるが tok.text が空だと意味がない
確認方法
isKeyword(tok.type) 自体は正しく動作する(import の type=130 に対して true を返す)。ただしハマりポイント2の通り tok.text が空なので、isKeyword が true でも書き込みが行われない。
type=130 isKeyword=true ← isKeyword は正しい
tok type=130 text= ← でも text は空
必ずハマりポイント2の対処とセットで使うこと。
動作確認済みの実装例
editor/syntax.d(抜粋)
void updateHighlight(dstring[] lines, TokenPropString[] props,
int changeStartLine, int changeEndLine)
{
import std.array : join;
import std.utf : toUTF8, toUTF32;
dstring fullText = lines.join("\n"d);
string src = toUTF8(fullText);
LexerConfig cfg;
cfg.stringBehavior = StringBehavior.source;
cfg.whitespaceBehavior = WhitespaceBehavior.include;
auto cache = StringCache(StringCache.defaultBucketCount);
auto tokens = getTokensForParser(cast(ubyte[]) src, cfg, &cache);
foreach (ref tok; tokens) {
int line = cast(int)tok.line - 1;
int col = cast(int)tok.column - 1;
if (line < 0 || line >= cast(int)props.length) continue;
if (props[line] is null) continue;
ubyte cat = toCategory(tok.type);
// ★ tok.text が空の場合は str() で補完
string rawText = tok.text.length > 0 ? tok.text : str(tok.type);
if (rawText.length == 0) continue;
dstring dtxt;
try { dtxt = toUTF32(rawText); } catch (Exception) { continue; }
int curLine = line;
int curCol = col;
foreach (dchar ch; dtxt) {
if (curLine >= cast(int)props.length) break;
if (ch == '\n') { curLine++; curCol = 0; continue; }
if (props[curLine] !is null && curCol < cast(int)props[curLine].length)
props[curLine][curCol] = cat;
curCol++;
}
}
}
ui/editor.d(抜粋)
// ★ load() の後に syntaxSupport をセット
ed.load(path);
if (detectFileType(path) == FileType.D) {
ed.content.syntaxSupport = new DSyntaxSupport();
// ★ 色は 0x00RRGGBB(アルファ 0x00 = 不透明)
ed.setTokenHightlightColor(TokenCategory.Keyword, 0x00FB4934);
ed.setTokenHightlightColor(TokenCategory.String, 0x00B8BB26);
ed.setTokenHightlightColor(TokenCategory.Character, 0x00B8BB26);
ed.setTokenHightlightColor(TokenCategory.Integer, 0x00D3869B);
ed.setTokenHightlightColor(TokenCategory.Float, 0x00D3869B);
ed.setTokenHightlightColor(TokenCategory.Comment, 0x00928374);
ed.setTokenHightlightColor(TokenCategory.Identifier, 0x00EBDBB2);
ed.setTokenHightlightColor(TokenCategory.Op, 0x00EBDBB2);
}
まとめ
| # | ポイント | 解決策 |
|---|---|---|
| 1 |
load() 後に syntaxSupport を上書きしてしまう |
load() の後に syntaxSupport をセット |
| 2 | キーワードの tok.text が空 |
str(tok.type) でフォールバック |
| 3 | dlangui の色アルファが逆 |
0xFF?????? → 0x00?????? に変更 |
| 4 |
isKeyword() は正しいがポイント2と組み合わせる必要あり |
ポイント2の対処とセットで使う |
dlangui のドキュメントは色フォーマットのアルファ仕様を明記していないため、ソースコードを直接読むのが確実です。