そうだ、奥多摩に行こう

理想のシンタックスハイライトを作る

2026年1月09日 VSCode DIY

Orbit

昨年末頃から勢いでVSCodeのテーマを作ってきた。スプラトゥーン3の武器モチーフが3つ、ポケモンのモチーフが1つ(しかし中に9種類ある)。これらは元ネタになる色があったので、エディタでコードを表示したときに、なんとなくモチーフの色になるように調整するだけでよかった。

それらに飽きたのでちゃんと作ってみようと思った。今回はシンタックスハイライトの話。



まず、トークンについて理解する

エディタがシンタックスハイライトを適用する際、コードを分析して各トークンに分け、トークンに対して事前に定義しておいた色を塗っていく、というブラウザみたいな動きをしている。

トークナイザーは2種類あり、TextMate形式とTree-sitter形式がある。後者はそれこそブラウザのようにコードを構文解析してASTを作る。これはVSCodeには入っておらず、zedとかNeoVimで採用されているらしい。VSCodeはTextMate形式なので正規表現によるマッチングになる。

ただVSCodeにもTree-sitterみたいな機能が入っており、Semantic Highlightingという機能として提供されている。文脈を読めるようになるのでいい感じに色を付けてくれる反面、描画に0.1秒くらいの遅延があるのでチラついて見えたり、定義が増えて画面上の色が増えすぎたりする弱点もある。試してみたけどチラつきが結構うっとうしく、ファイルを開いたときだけでなく、大きなファイルをスクロールしたときにも発生することがある。

またLSP(言語サーバー)が無いと動かないので、Semantic Highlightingに頼って色を定義するのは悪手っぽい。ということで、基本は正規表現の方でなんとかして、余力があったらSemantic Highlightingを使う、という方針にする。

プログラムも言葉なんだよな…?

英文を読むときは主語をまず認識して、次に動詞を見る。

Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, “and what is the use of a book,” thought Alice “without pictures or conversations?”
Alice’s Adventures in Wonderland | Project Gutenberg

こういう文章があったときに、各文ごとに主語を見る。

Alice was beginning to get very tired of ... and of having nothing to do: once or twice she had peeped into the book ...

主語を見つけたら次に動詞を見る。

Alice was beginning to get very tired of ... and of having nothing to do: once or twice she had peeped into the book ...

「アリスが」「何かしたんやな」とザッピングして、「何か」に当たる部分を残りのテキストのところから拾う。

Alice was beginning to get very tired of ... and of having nothing to do: once or twice she had peeped into the book ...

OOP言語では、コードは「主語」「動詞」「目的語」の羅列になる。英語の基本文型のSVOになるが、これはプログラミング言語が英語圏生まれだからだろう。関数型は数学っぽいので別物として、ここでは考えない。

英語だと頭に入りづらいので 俺のこの手が真っ赤に燃える 勝利を掴めと轟き叫ぶという文章があったとする。これを意味が伝わる状態のまま削っていくと 俺の手 / 燃える / 勝利 / 掴め / 叫ぶ になる。

こういうコードがあったとする。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

実際、1ファイルにこういうものがたくさんある。コードリーディングでざっと読む(というか見る)時にどこに注目しているかというと、多分主語である。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

次に動詞を見る。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

残った目的語にも色を付ける。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

プログラミング言語は構造を表していて、文(statement)をifなどの制御構文で分岐・合成している。文も大事だけど、制御構文はさらに上位の存在なので、主語と同じ色のちょっと明るいものにして目立たせる。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

という方針がいいかな?と思った。

ちょっと脱線して色の話

Linear visible spectrum.svgGringer - 投稿者自身による著作物, パブリック・ドメイン, リンクによる

ヒトが認識できる光の波長は上のようになっている。紫より左に行くと「紫外線(Ultraviolet)」、赤より右に行くと「赤外線(Infrared)」になる。

人間がキラキラしたものを好むのは、原始人時代に光を反射するもの=水だったから、みたいな物を以前どこかで読んだような気がしたが、あんな感じで人間が見える色とか好みには何らかの由来がある(なので、目以外のセンサー性能が良い犬とかは錐体の種類が少なく赤〜緑の区別がつかないという話もある(要出典))。

可視光の図を見ると、ヒトにとって重要なのは黄緑で、あと赤が見えていればOK!みたいに見える。緑が強いのは新鮮な木になっている実とか食べてた名残なのかな?赤は血とか熟した果実、青は海とか空とかだろうか。情報量の少ない背景っぽい。

見方を変えて、色を文化による後付け情報として見ると、赤は禁止、黄色は注意、緑が安全、青が指示、などになる。JIS安全色だと紫が放射能らしい。

主な用途
禁止、危険、防火、緊急、停止
危険、警告、航空・船舶の保安施設
注意警告
安全状態、救護、進行
指示、誘導
放射能

JIS安全色 - Wikipedia

またここで見方を変えて、錐体が認識する光の三原色RGB(赤緑青)について考えてみる。ディスプレイでR:100, G:0, B:0 ( #ff0000 ) などにすれば、LEDの1つをフルパワーで使えるから目立つのでは?と考えていた時期が僕にもありました。

File:Color circle (RGB).svgBy Crossover1370 - Own work, CC BY-SA 4.0, Link

光の三原色は混ぜたら白になるので一番眩しいのだけど、同じ理屈でRGBそれぞれの間にある中間色のほうがRGB単体よりも明るく表現できる。上の色相環でも Y の字の形で明るい線が見える。シアン、マゼンタ、イエローのCMYだった。

シンタックスハイライトのプロトタイプ

シンタックスハイライトのプロトタイプ

とりあえず「特定の語を目立たせたい」「文化的な意図も拾っておきたい」と考えていたので、プロトタイプテーマの中で主語、動詞、目的語の色付けに使うことにした。

警告色の赤を中心に見ていって、指示色の青のメソッドで動作をイメージして、注意色の黄色の値で内容を完全に理解する、という意図で作っていたが、そんな風に読めるんだろうか?本当に?慣れか?

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

明るいピンクでザッピングして、主語を見つけて、動詞と目的語をみたら「あっ、ゴッドフィンガーだな」とわかる。

今回はダークモードだけを作るつもりでいたので、明度も識別の道具に使える。同じ赤系でも export, if, return などの分岐点は目立たせる、function, const などは控えめにする。

動詞は動詞なので1パターンのまま。黄色の目的語のうち、変数・数値・文字列は明るく、型情報などは暗くする(スクショでは数値が暗くなっているけど気にしないで)。

このあたりは背景も暗いことから星に例えていて、恒星、1等星、2等星などと呼んでいた。配列の [ ] やオブジェクトのブレース、セミコロンなどはそんなに見えなくていいのでダークマター、型定義などは黄色だけど優先順位が低いので遠くの方の星団、とか。

ふつうのデザインと同じように視線誘導できるよう、コード内の優先順位を付けて色を定義していった。

ちょっと脱線して、太字、斜体の話

シンタックスハイライトで使える道具は、色の他に太字と斜体がある。Rosé PineCatppuccinでは斜体がよく登場する。MatteBlackでは太字が多用されている。

色だけで表現しきれない場合にこれらを使うことになるが、読みやすさが結構変わってしまう。

新機動戦記
ガンダムW
Endless Waltz
新機動戦記
ガンダムW
Endless Waltz
新機動戦記
ガンダムW
Endless Waltz

太字は面積が増えて目立ちすぎるので使わないようにし、斜体は小さい違和感を生むので import export などファイル外とのやり取りにのみ使うようにした。

テーマとしての色を決める

ここまでは機能上の理由で色を決めていた。機能性を保ったままいい感じの色にしたい。しかしなかなかうまく行かない。

マゼンタに寄せて色を調整していったら女性用化粧品みたいになり、

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

色相をちょっとずつズラしてみたら初号機になったりした(ピッコロさんに見えないこともない)。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

色減らしたらどうかな?と減らしたらそれっぽくなったので、これで良しとした。危険色の赤の代わりに明度、指示色の青は青緑、値の黄色は黄緑。

もし 持っている 明鏡止水の心
  俺の手 燃える()
    .then(() => 叫ぶ(掴め 勝利))
そうでないなら
  俺の手 唸る()
    .then(() => 叫ぶ(倒せ お前))

Orbit

冒頭の画像を再掲するが、最終的にこのようになった。明るい白を見て、青緑色を見て、黄緑を見る。うまくいってそうな気がする。演算子は意図的に明るくしているが、目立ちすぎているので要調整である。

星とか天体で例えながら作っていたので、テーマ名は Orbit にした。

他のテーマの色: 特に Atom / Solarized

他のテーマの色

他のテーマの色

ここまでの作業と並行して、他のテーマのシンタックスカラーも調べていた。テキストは緑、数値はオレンジ、メソッド名は青、記号はグレー、という傾向がなんとなくあった。

これらは Atom One Dark の色を踏襲してこうなっている。

Hexad

Atom系は色相をまんべんなく使っているヘクサード配色になっている。Ayuはオレンジと緑の出現率が高いのでミカンっぽいな🍊と思っていたが、よく調べてみるとヘクサードになっていた。

Solarized

Solarized color

Atom One Dark が出るまでは、エディタやターミナルのテーマと言ったら Solarized だった。

“完璧”なカラー設定「Solarized」の魅力は、計算し尽くされたものだった | WIRED.jp という記事や公式サイトの図にあるように、ダークとライト共通で使えるカラーパレット、こだわりの色彩設計、CIELAB色空間を使って人間の知覚通りに揃った明るさ、低コントラストで目に優しい、という特徴がある。

ただこれが出たのは2011年ごろで、MacだとSnow Leopardの頃になる。当時のディスプレイ環境だと快適だったような気がするけど、今だと暗すぎる、コントラストが低すぎる、カラーパターンもAtom系と違っていてなんとなく気持ち悪い、という感想になってしまった。

アクセントカラーも特定の色を組み合わせると、コンプリメンタリー(補色)、トライアド(3色)、テトラード(4色)のちょうどいい関係になるらしく、上のスクショだと緑、青、赤のトライアドになってるっぽい。しかしそれがいいかと言うと…。特に赤のハレーションが気になる。

他のテーマの配色

他のテーマの配色

気に入っているテーマの配色を見ると、テトラード、トライアドに収まっていることが多かった。Tokyo NightとかSlimeとか、色数が多いのにうまく収まっている理由はおそらくこのへん。

スプリットコンプリメンタリー(っぽいもの)の2つは自分が作ったテーマで、これらも色数が少ない。少ない中でなんとかやりくりしているので見た目はスッキリしているが、別の意味のものに同じ色を割り当てていることもあるので、使いやすいかどうかは微妙である。

CIELAB色空間のLとコントラスト

RGBやCMYKとは異なり、Lab色空間は人間の視覚を近似するよう設計されている。知覚的均等性を重視しており、L成分値は人間の明度の知覚と極めて近い。
Lab色空間 - Wikipedia

SolarizedはCIELAB色空間で明るさを整えた…とあり、アクセントカラーの明るさのL値は50前後だった。Lab色空間はPhotoshopで調べられるのでいくつか見てみると、Atom One Darkは60〜70くらい、Tokyo Nightとかモダンなテーマは70〜80くらいだった。

RGB - Lab

また、Solarizedは無彩色寄りのグレーをLabカラーで5ずつ(たまに10ずつ)変えている。無彩色ならRGBでもだいたい同じじゃない?と思って試してみたところ、L30〜70くらいではRGBの方が明るく出てしまうが、L値で言うと1〜2程度しか変わらない結果になった。Labモードのカラーツールも見慣れていなくて使いづらく感じるので、ここにこだわるのはそのうちでいいかな…と思って見送った。

Contrast

問題は明るさよりもコントラストである。自分の感覚では、Atom One Darkはちょうどよく、Solarizedは暗く、Tokyo Nightなどのモダンなテーマは眩しく感じていた。Figmaのプラグインでコントラストを確認したところ、眩しいと感じるテーマはWCAGが定義するコントラストにおいて、通常テキストサイズでもAAAを取るものが多かった。Atom One DarkはAAを取ったり取らなかったりするレベル、Solarizedはもっと多くの色がFailになっていた。

コントラスト比で言うと、Atom系で落ち着いたものは6くらい、Solarizedは4くらい、Night系で眩しく感じるものは9くらい、かなり眩しく感じるJellyFishが20くらいだった。

自分の欲しいシンタックスハイライトは視線誘導ができるもので、全部の色が目立つ必要はない。情報を立体的に取得したい。という方針で色を決めて、通常の色がついている部分は平均7くらい、目立たせたい一部分だけ15の高コントラストを採用している。

という感じで

雰囲気で作ってきていたテーマに、初めて意図を入れてみたという話でした。視線誘導を意識したテーマってなさそうなので、うまく機能すると良いですね。
Orbitは自分でも気に入っていてドッグフーディングできているので、今後もアップデートしていく予定です。他は気が向いたら。

Publisher monoooki - Visual Studio Marketplace / Open VSX Registry (Kiro, Cursor)