CodeMirrorは、バージョン5以前とバージョン6とで、根本的な設計が変わっている。具体的には、アタタタッとグローバル変数でなんとかする設計から、ヒャォゥッと細かいモジュールに分割する設計に変わっている。どこが具体的だ。しかもアタタタッの方も最終的には肉片というモジュールに分解されるので、混乱のもとだ。

さておき、いったんCodeMirror6.x系の基本的な知識や使い方をまとめておこうと思う。

CodeMirror6と5以前との比較

これまでのバージョンと現行バージョンでは、おおよそ以下のような違いがある。

項目 CodeMirror6以降 CodeMirror5以前
モジュールシステム ES Modules / CommonJS 非対応
パッケージ @codemirrorスコープ下で細分化 codemirrorでほぼ完結
設定 拡張(Extension)で指定 オプションオブジェクトで指定
状態管理 イミュータブルなオブジェクト(EditorState)で更新 エディタインスタンスを直接変更
ビュー ビューインスタンス(EditorView)で制御 エディタインスタンスで制御
テキストエリアとの連携 専用メソッドでテキストエリアと同期(エディタを作るだけならテキストエリアは不要) 既存のテキストエリアをもとにエディタ作成

ざっとまとめただけでも、諸機能がモジュール化されていることがわかる。正直、5の方がとっつきやすい。6は追いかけるモジュールが多く、大枠を理解するまでがなかなか大変である。というか絶賛大変中で、まだ大枠を理解できていない。

改変点が多いことに配慮して、公式リファレンスにはCodeMirror6の設計概要がまとめられたページが用意されている。私はなぜかこのページをすっ飛ばし、適当にドキュメントをつまみ読みして迷宮に迷い込んでしまった。はじめにこれを読んでいたらだいぶ違ったであろう。

CodeMirror6の基本知識

CodeMirror6は、@codemirror下のパッケージ群で構成される。これらを個別に管理するの手間であるため、最低限必要な依存関係が無印のcodemirrorにまとめられている。これをインストールすれば、手始めに必要なモジュールの大部分が芋づる式に、もとい、イモジュール式にインストールされる。

どシンプルなエディタだけであれば、codemirrorパッケージだけで足りる。ただ、任意の言語をサポートする場合は、その言語のパーサやらなんやらが必要である。そういった言語の各種情報は、@codemirror/lang-xxxというパッケージで提供される(xxxには言語名が入る)。手始めに、HTMLの言語サポートパッケージを使ってみる。

bash
npm i -D codemirror @codemirror/lang-html

CodeMirror6では、エディタはEditorViewオブジェクトとして表現される。このオブジェクトは、エディタの初期状態(あるいは初期状態の生成に必要な情報)を受け取って生成される。初期状態が渡されなかったときは、内部で自動的に初期状態がセットされる。

ミニマルなエディタのサンプルコード

以下は、ドキュメントの内容と、codemirrorパッケージの中に用意される基本的な拡張のセット、HTMLの言語サポート機能(こちらも拡張)を渡してエディタを作成するサンプルコードである。

js
// import { EditorView, basicSetup} from "codemirror";
// import { html } from "@codemirror/lang-html";
import { EditorView, basicSetup, html } from "/assets/js/codemirror6-bundle.js";

const htmlMixedCode = `<style>p{font-size:16px;}</style>
<p>CSSファイルの読み込みは<em>不要に</em>なりました。</p>
<script>
  function hi(xx){console.log("Hello, " + xx);}
  hi('CodeMirror6');
<\/script>`;

const view = new EditorView({
  doc: htmlMixedCode,
  extensions: [basicSetup, html()],
  parent: document.body
});

現在(2023年7月)、CodeMirror6をホスティングしているCDNサービスは見当たらない(後述)。そもそも、6はバンドルされることを前提にモジュール化されているようである。

上記のサンプルコードでは、デモを表示するための苦肉の策として、必要なモジュールをバンドルしてインポートしている(このブログでは、サンプルコードをiframeにねじ込んでデモを表示している)。本来は、コメントアウトしているimport文を使う。

Version 5では、CSSやJavaScirpt混じりのコードをハイライトするために、XML、JavaScript、CSSの各モードが必要だった。CodeMirror6ではlang-htmlパッケージだけで、styleタグ下のCSSとscriptタグ下のJavaScriptがハイライトされる。これは、HTMLの言語パッケージの内部で、CSSとJavaScript(とTypeScriptとJSXとTSX)の言語パッケージを読むことで実現されている。

バンドルせずに使う方法

明敏な有志によって、バンドルせずにCodeMirror6を使う方法も形にされている。

html
<script type="importmap">
	{
		"imports": {
			"codemirror/": "https://deno.land/x/codemirror_esm@v6.0.1/esm/"
		}
	}
</script>
<script async type="module">
	import { basicSetup, EditorView } from "codemirror/codemirror/dist/index.js"
	import { html } from "codemirror/lang-html/dist/index.js"

  const htmlMixedCode = `<style>p{font-size:16px;}</style>
<p>CSSファイルの読み込みは<em>不要に</em>なりました。</p>
<script>
  function hi(xx){console.log("Hello, " + xx);}
  hi('CodeMirror6');
<\/script>`;

  const view = new EditorView({
    doc: htmlMixedCode,
    extensions: [basicSetup, html()],
    parent: document.body
  });
</script>

エディタを表現するEditorView

CodeMirror6では、エディタはEditorViewオブジェクトとして表現される。EditorViewコンストラクタにエディタの設定に必要な情報を渡せば、インスタンス化と同時にエディタがレンダリングされる。

エディタの設定に必要な情報は、以下のようなEditorViewConfigインタフェースに準拠したオブジェクトとして表現される。

ts
interface EditorViewConfig extends EditorStateConfig{

// ビューの初期状態。`EditorView`の`state`プロパティにそのまま格納される。
// 与えられない場合、`EditorView`コンストラクタは内部で
// `EditorState`オブジェクトを生成し、`state`プロパティに格納する。
state⁠?:EditorState

// エディタを追加する親要素。
// 与えられなくともEditorViewオブジェクトは作られるが、
// エディタを表示するには自分で追加する必要がある。
parent⁠?:Element | DocumentFragment

// エディタをマウントする要素
// デフォルトは`document`。
// `ShadowRoot`にエディタをマウントすることもできる。
root⁠?:Document | ShadowRoot

// エディタの更新を捕まえて任意の処理をしたいときに使う。
// トランザクションはエディタの更新単位(リドゥとかアンドゥにも使割れる)。
dispatch⁠?:fn(tr: Transaction)
}

引用元:EditorViewConfigインタフェースの定義

先のサンプルコードでEditorViewコンストラクタに渡しているオブジェクトには、EditorViewConfigインタフェースの定義にないdocプロパティとextensionsプロパティが含まれている。

js
const view = new EditorView({
  doc: htmlMixedCode // <-これと
  extensions: [basicSetup, html()], // <-これ
  parent: document.body,
});

これらは、EditroViewConfigインタフェースが継承するEditorStateConfigインタフェースで定義されている。

ts
interface EditorStateConfig {
  // 初期化する際にエディタに表示するテキスト。
  doc?: string | Text;

  // 初期化する際のカーソルの表示位置。
  // デフォルトではドキュメントの一番最初に設定される。
  selection?: EditorSelection | { anchor: number; head?: number };

  // エディタに追加する拡張。
  extensions?: Extension;
}

引用元:EditorStateConfigインタフェースの定義

これらのオプションをすべて省略しても、EditorViewオブジェクトは作られる。ただその場合、機能的にはテキストエリアと変わらないエディタが、宙ぶらりんで変数に収まることになる。羽をもがれた蝶、あるいは穴のないうまい棒のように、CodeMirrorエディタのアイデンティティが失われてしまう。かわいそうだ。最低限Extensionsプロパティは指定しておきたいところである。

機能を担うExtensions

例えば以下のように、エディタにはさまざまな機能が期待される。

  • シンタクスハイライト
  • ブロックの折りたたみ
  • キーボードショットカット
  • テキストの選択・置換
  • オートコンプリート…etc

CodeMirror6では、これらは拡張(Extension)という形で個々に提供される。

拡張は、エディタの状態を管理したり、状態の変更が必要になったときに特定の動作を行う機能を提供する。CodeMirror6では、エディタを作る際に任意の拡張を設定オブジェクトのextensionsプロパティに渡し、エディタの諸機能を実現する。ネストされた拡張は、処理する段階でフラットに直される。

先のサンプルコードで指定したbasicSetupも、複数の拡張をまとめた配列変数である。名前の通り、これにはエディタが必要とするであろう拡張が一通り詰まっている。 ちなみにbasicSetupに含まれている拡張は、以下の通りである。

bascSetupに含まれる拡張の一覧

js
const basicSetup: Extension = (() => [
  // エディタに行番号を追加する。
  lineNumbers(),
  // アクティブな行のガター(行番号横の余白)をハイライト可能にする。
  highlightActiveLineGutter(),
  // 通常見ることのできない、特殊な空白、改行文字等をハイライト可能にする。
  highlightSpecialChars(),
  // キーコマンドに応じてアンドゥ・リドゥを可能にする。
  history(),
  // コードブロックを折りたたみ可能にする。
  foldGutter(),
  // テキスト選択時に、エディタと同じ幅の矩形の選択範囲を描く。
  drawSelection(),
  // エディタ上を何かがドラッグされているとき、ドロップ候補の箇所にカーソルを表示する。
  dropCursor(),
  // 有効化すると、エディタで複数の範囲を選択できるようにするファセット。
  // ただし、デフォルトではエディタはネイティブのDOM選択に依存しており、
  // 複数の選択を扱うことができない。
  // そのためこのファセットは、drawSelection()のような拡張を併用する必要がある。
  EditorState.allowMultipleSelections.of(true),
  // `Language`オブジェクトの`languageData.indentOnInput`フィールドに
  // 正規表現が格納されている場合にのみ作用する。
  // 新しく入力されたテキストの行頭からカーソルまでの入力が、
  // その正規表現に合致した際に、自動インデントを行う。
  // なお、不要な再インデントを避けるために、
  // 同フィールドの正規表現は`^`で始め、$で終わらせることが推奨される。
  indentOnInput(),
  // highlighterをラップし、
  // それを使ってエディタにシンタクスハイライトを適用する。
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
  // ブラケット(`[]`、`{}`、`()`、`""`、`''`)の
  // 隣にカーソルがあるときに、反対側もハイライトする。
  bracketMatching(),
  // ブラケットを入力したら自動で閉じる。
  closeBrackets(),
  // オートコンプリートを有効にする。
  autocompletion(),
  // Altキー(or optionキー)+選択で、
  // テキストの矩形選択を有効にする。
  rectangularSelection(),
  // Altをデフォルトとする修飾キーが押されているときに、
  // 十字キーを表示する。
  crosshairCursor(),
  // アクティブな行をハイライトする。
  highlightActiveLine(),
  // 選択したテキストと一致するテキストをハイライトする。
  highlightSelectionMatches(),
  // キーマップを受け取り、基本的なキーバインディングを有効にする。
  // basicSetupでは以下のキーマップが渡されている。
  keymap.of([
    ...closeBracketsKeymap,
    ...defaultKeymap,
    ...searchKeymap,
    ...historyKeymap,
    ...foldKeymap,
    ...completionKeymap,
    ...lintKeymap,
  ]),
])();

引用元:basicSetupの定義

何気ないエディタに、こんなにたくさんの機能が含まれていたのかと驚かされる。ちなみにファセット(=Facet)は「側面」や「面」を意味する英単語で、CodeMirror6ではファセットは拡張が共有する変数を管理するための機能を指す(まだ理解が追いついていないため、間違っているかもしれないが)。

また、拡張が同じ状態に対して動作を行う場合、拡張がextensionプロパティに渡された順番によって優先度が決まる。つまり、先に渡された拡張の動作が先に適用される。

CodeMirrorの設定に使える拡張は、公式サイトのList of Core Extensionsに一覧がある。また、リファレンスにも、拡張を返す関数がたくさん見られる。これらを組み合わせて、自前で作成することもできる。

言語情報を担うLanguageSupport

折りたたみやハイライト、オートコンプリートといった一部の拡張は、単体だと機能が発揮されない。エディタに指定された言語の諸情報(パーサーなど)を内部で参照して、各言語に応じた処理を行うからである。この言語の諸情報は言語サポートパッケージと呼ばれ、やはり拡張として提供される。最初にインストールした@codemirror/lang-htmlがそれに当たる。

以下は、CodeMirror6で用意されている言語パッケージの一覧である。

あとは、コミュニティによりいくつかの言語サポートパッケージが提供されている。いずれにせよCodeMirror5よりだいぶ少ない。欲しいものがなければ、自分で作るしかない。私はPugを扱いたいので、そのうち挑戦したいと思う。

まとめ

CodeMirror6では、エディタをEditorViewオブジェクトで表現し、その機能はExtensionと呼ばれる種々雑多なオブジェクトにより個別に追加される。

折りたたみやシンタクスハイライト、オートコンプリートといった、言語に依存する機能は、各言語に共通する部分と言語に依存する部分とに分離され、それぞれ別個の拡張にまとめられている。両方追加しなければ本領が発揮されない。なお、言語に依存する部分をまとめた拡張は言語サポートと呼ばれる。

CodeMirrorエディタの細かい操作は、おいおい学んでいきたいと思う。

参考資料