TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

新しくなったCodeMirror v6で遊ぼう!

こんにちは。Synergy!というサービスのフロントエンドを主に開発している渋谷です。
今回はSynergy!でも利用しているCodeMirrorというライブラリの紹介です。
皆さんはCodeMirrorを使ったことがありますか?CodeMirrorとはコードエディターをWeb上で扱うためのライブラリです。WYSIWYGエディタではなく、どちらかといえばVS Codeの内部で利用されているMonaco Editorなどの仲間です。
CodeMirrorではコードエディターの作成以外にも以下のようなtextareaやinputのコンポーネントを作る事ができます。

  • 特定の文字列が入力されたときにその文字列にマークをつけたい
  • 自前で入力補完を実装したい

CodeMirrorは非常に柔軟性が高く様々な要件をカバーできる可能性があります。ただ、現在のところあまり記事が出回っていないので、CodeMirror v6の遊び方を書いてみました。
それでは始めましょう🔥

渋谷 拓正(シブヤ タクマサ)
React + TypeScriptが好きです。最近メガネが壊れたので変えました。

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.createextensionsに渡すことで使用することができます。
例えばplaceholderであれば以下のようにします。

import { EditorState } from "@codemirror/state"
import { EditorView, keymap, placeholder } from "@codemirror/view"

const startState = EditorState.create({
  doc: "Hello World",
  extensions: [placeholder('入力してください')]
});
...

extensionsExtensionを渡すことができ、基本的には記載されている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.dispatchTransactionを渡し、変更内容を投げます。そうすると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()に渡すことで、エディタがアップデートされたタイミングでこの処理が呼ばれるようにしています。
エディタのアップデートは単に入力が起こった時以外にも様々な場合を含むため、引数で渡されるViewUpdatedocChangedviewportChangedを利用して、内容が変更された時、または表示範囲が変更されたときに絞っています。
もし入力内容に応じて何かを行いたい場合(例えばReactから変更を検知したい場合)は、EditorView.updateListener.of()を使って同様の処理を行うことになるでしょう。
今回はeffectsをdispatchしましたが、例えばchangesをdispatchし、特定の文字列を変更するという処理も可能です。

addStrongDecorationStateFieldを使って発行しました。
これを使うことで今回で言えば、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することもできます。
僕自身は試したことがありませんが、共同編集可能なエディタも作成が可能だったり、非常に表現力が豊かなエディタになっています。

これを機に是非遊んでみてください🙆‍♂️

シナジーマーケティング株式会社では一緒に働く仲間を募集しています。