こんにちは。Synergy!というサービスのフロントエンドを主に開発している渋谷です。
今回はSynergy!でも利用しているCodeMirrorというライブラリの紹介です。
皆さんはCodeMirrorを使ったことがありますか?CodeMirrorとはコードエディターをWeb上で扱うためのライブラリです。WYSIWYGエディタではなく、どちらかといえばVS Codeの内部で利用されているMonaco Editorなどの仲間です。
CodeMirrorではコードエディターの作成以外にも以下のようなtextareaやinputのコンポーネントを作る事ができます。
- 特定の文字列が入力されたときにその文字列にマークをつけたい
- 自前で入力補完を実装したい
CodeMirrorは非常に柔軟性が高く様々な要件をカバーできる可能性があります。ただ、現在のところあまり記事が出回っていないので、CodeMirror v6の遊び方を書いてみました。
それでは始めましょう🔥
CodeMirror v6
CodeMirror v6は長い間メジャーバージョンが0でしたが、2022年の6月に6に上がりました。
もともとはCodeMirror v5が長い間使われていました。ただ、時代が進むとともにアーキテクチャが古くなってきており、v6で全面的にリライトされることになりました。
v5とv6は全くの別物となっており、互換性がありません。(なお、v5からのマイグレーションガイドが公開されています。)
v6はTypeScriptで書き直され、modulesも細かく分解されました。状態の変更はReduxやElmを参考に考えられているので、Reactに慣れ親しんだ開発者にとっては理解しやすいかもしれません。
System Guideが公開されているので、予め読んでおくことをおすすめします。
ドキュメントの歩き方
以下が公式ドキュメントです。
開発を始めるとわかりますが、ドキュメントを読んでも残念ながら何をすればいいのかすぐには理解が難しいです(僕はそうでした)。また、CodeMirror v6はあまり参考記事みたいなものが見つからず、理解のきっかけを探すことが難しい状態でもあります。
というわけで僕は以下のような形で開発を進めていきました。
- examplesを読んで似たようなサンプルを探す
- => 関係ありそうなワードをpickする
- pickしたワードからReference ManualのAPI群を眺める
- forum内をワードで検索をかける
- 型情報を確認する
基本的にexamplesで雰囲気を掴み、referenceで言葉を覚えてforumを漁る、みたいなことを繰り返していました。それ以外にも型に非常に多くのコメントが残されているため、型から得る情報も有益でした。
examplesの掲載コードは非常に断片的で、おそらく一読しただけではどうしたらいいのかわからない、という状態になると思います。
そういうときはcodemirror/website
のソースに当たることで、実際にどう使えばその形になるのかを知ることができます。
Hello world
CodeMirrorは以下の3つがコアになっています。
@codemirror/state
=> エディタの状態を扱うためのmodule@codemirror/view
=> エディタの表示や操作の部分を扱うためのmodule@codemirror/commands
=> エディタでのキーボード操作を扱うmodule
基本的な使い方に関してはこちらに記載がありますが、以下のとおりです。
import { EditorState } from "@codemirror/state" import { EditorView, keymap } from "@codemirror/view" import { defaultKeymap } from "@codemirror/commands" const startState = EditorState.create({ doc: "Hello World", // 初期値を指定 extensions: [keymap.of(defaultKeymap)] // 拡張機能を追加 }); const view = new EditorView({ state: startState, parent: document.body }); // これでparentに指定したDOMにCodeMirrorをマウント
view
にはEditorView
が入っています。
これを使うことで、CodeMirrorを操作することが可能です。
// destroy view.destroy(); // 値をinsert view.dispatch({ changes: { from: view.state.selection.main.head, insert: '追加したいテキスト', }, }); // 内容を置き換え view.dispatch({ changes: { from: 0, to: view.state.doc.toString().length, insert: value, }, });
EditorView
はプロパティとしてEditorState
も持っているので、EditorView
があると多くのことができます。EditorView
には他にもたくさんのプロパティやメソッドが用意されています。
EditorVIew
moduleのインストール
v6では前述の通り細かいmoduleに分けて提供されており、それぞれを組み合わせて使うことになります。
Reference Manualの左側のナビゲーションに大項目がおいてありますが、大体その項目でモジュールが分けられています。
またbasic-setup
を使うことで、よく使われていそうなモジュールをまるっと使用することも可能です。basicSetup
ただ、basic-setup
から不要なものを間引く方法がないので、実際には必要なものだけをinstallするほうが良いかもしれません。
ライブラリのアップデート
CodeMirrorは多くのライブラリに分かれている性質上バージョン管理が少々面倒です。牧歌的に更新しようとした場合に、タイミングによっては複数のバージョンの@codemirror/state
が入ってしまう場合があります。その時は起動時に以下のようなエラーになります。
Uncaught Error: Unrecognized extension value in extension set ([object Object]). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.
そういう場合はCodeMirror関係のものをすべて消して入れ直すと、多くの場合は解消されます。
extensionで遊ぼう
CodeMirror v6はextensionで拡張することでさまざまな目的を実現することが可能です。
実例を見ながらCodeMirrorで遊んでみましょう。
extensionことはじめ
■ 様々なextension
すでに存在するextensionはこちらにまとまっています。
これをEditorState.create
のextensions
に渡すことで使用することができます。
例えばplaceholderであれば以下のようにします。
import { EditorState } from "@codemirror/state" import { EditorView, keymap, placeholder } from "@codemirror/view" const startState = EditorState.create({ doc: "Hello World", extensions: [placeholder('入力してください')] }); ...
extensions
はExtension
を渡すことができ、基本的には記載されているextensionはExtension
を返します。
Extension
は以下のように定義されており、配列で渡すことが可能です。
declare type Extension = { extension: Extension; } | readonly Extension[];
たまに記載されているextensionの中にはFacet
を返すものがあります。Facet
はエディターの状態に関連するラベル付きの値を扱うためのものでof
というメソッドを持っており、それを呼ぶことでExtension
が返ります。
例えば、readOnlyにしたい場合は以下のようにします。
const startState = EditorState.create({ doc: "Hello World", extensions: [EditorState.readOnly.of(true)] }); ...
必要なextensionはこんな感じで登録していきましょう!
■ 状態の変更
CodeMirrorは前述の通りReduxやElmを参考にしている部分があります。
それがこの状態変更の部分です。
状態の変更はeditorView.dispatch
を通して行われます。
editorView.dispatch
にTransaction
を渡し、変更内容を投げます。そうするとextension側にはTransaction
が流れてくるので、それを元に行いたい処理をします。
ちょうどReduxがactionをdispatchし、reducerで受け止める流れをイメージしてもらうとわかりやすいと思います。
以下、実例を見ながら感触を掴んでいきましょう。
extensionを実際に作ってみよう
今回はDecoration
を付与するextensionを作成します。
CodeMirrorのextension作成における、非常に多くの要素が含まれているため、他のextensionを作るときにも役立つと思います。
■ 特定のテキストに装飾をつけるextension
v5まではMarkという呼び方で呼ばれていましたが、v6ではDecoration
という名前で呼ばれるようになりました。こちらに例があります。
Decoration
の作り方としては4種類ありますが、ここではMark
を取り上げます。
Mark
は表示されている文字列をhtmlタグ(通常span)で囲み、装飾することができます。以下ではstrong
要素が入ってきたときに、背景を赤にするというDecoration
を作ります。というわけでCodeSandboxを用意しました🙆♂️
strongDecorationExtension.ts
が中身です。
これはexampleのUnderlining Commandをベースに作成しています。
strongDecorationExtension
は主に2つの部分から成り立っています。
- strongDecorationEffect =>
effect
をdispatchするための関数 - strongDecorationField => dispatchされた
effect
を受け取り、decorationを実際に作成する
StateEffect
strongDecorationEffect
はそれ単体ではExtensionではないため、EditorView.updateListener.of()
に渡すことで、エディタがアップデートされたタイミングでこの処理が呼ばれるようにしています。
エディタのアップデートは単に入力が起こった時以外にも様々な場合を含むため、引数で渡されるViewUpdate
のdocChanged
とviewportChanged
を利用して、内容が変更された時、または表示範囲が変更されたときに絞っています。
もし入力内容に応じて何かを行いたい場合(例えばReactから変更を検知したい場合)は、EditorView.updateListener.of()
を使って同様の処理を行うことになるでしょう。
今回はeffects
をdispatchしましたが、例えばchanges
をdispatchし、特定の文字列を変更するという処理も可能です。
addStrongDecoration
はStateField
を使って発行しました。
これを使うことで今回で言えば、strongDecorationField
から、発行されたeffect
がこのfieldで処理すべきものなのかを判別することができます。(effect.is(addStrongDecoration)
という形で検証ができます)
今回は簡易的にfrom、 toしか持ちませんでしたが、それ以外の様々な情報を持つことが可能です。
syntaxTree
もし、何かしらの言語機能を利用している場合は、テキストを解析する方法としてsyntaxTree
が利用できます。
今回はlang-html
を利用していたため、html
としてテキストが解析され、その結果を用いてeffectをdispatchしました。
何も使用していない場合は空のtreeが返されます。
StateField
これは発行されたTransaction
を受け取り、何かしらの処理を行うためのものです。今回はDecoration
を作りましたが、例えばTooltipを作る場合にも似たような方法で行うことができます。
update
には以前の値と、Transaction
が引数で渡ってきます。
それを元に、新しい値を返すことになります。
provide
ではこのfieldがどの様に扱われるかを指定します。今回はDecoration
を作りたかったのでEditorView.decorations.from()
を使いました。
Tooltipを使う場合はshowTooltip.from
などを使うことになると思います。
Reactで使うには?
あまりデファクトスタンダードだと言えるライブラリはなさそうですが、Reactでももちろん使うことができます。
他にも方法はあると思いますが、例えば以下のようにすることで使うことが可能です。
もっといい方法あれば教えて下さい🙏
まとめ
なんとなくCodeMirror v6のイメージが掴めたでしょうか?
紹介しきれませんでしたが、例えばEditorView.domEventHandlers
を利用し、エディタ内のDOMイベントを拾い、変更をdispatchすることもできます。
僕自身は試したことがありませんが、共同編集可能なエディタも作成が可能だったり、非常に表現力が豊かなエディタになっています。
これを機に是非遊んでみてください🙆♂️