zudo-codemirror-wisdom

Type to search...

to open search from anywhere

設定駆動型エディター構成

作成2026年4月3日Takeshi Takatsudo

アプリケーション設定オブジェクトから CodeMirror を完全に設定し、ランタイム再設定にはコンパートメントを、バリデーションには共有スキーマを使用するパターン。

設定駆動型エディター構成

このレシピでは、アプリケーション設定オブジェクトから CodeMirror エディターを完全に設定するパターンを解説します。すべてのエディター動作(フォント、インデント、機能トグル)が単一の設定ソースから供給され、共有スキーマによってバリデーションされます。

設定オブジェクト

エディター設定は型付きフィールドを持つプレーンオブジェクトです。

interface EditorSettings {
  fontFamily: string;
  fontSize: number;
  lineHeight: number;
  paddingHorizontal: number;
  paddingVertical: number;
  indentType: "tab" | "spaces";
  indentSize: number;
  vimMode: boolean;
  typewriterScrolling: boolean;
  showIndentGuides: boolean;
  bracketColorization: boolean;
  markdownListIndent: boolean;
  listHangingIndent: boolean;
  showStatusBar: boolean;
}

デフォルト値は共有パッケージで定義され、フロントエンドと設定バリデーションロジックの両方が同じデフォルトを使用します。

const defaultEditorSettings: EditorSettings = {
  fontFamily: "Menlo",
  fontSize: 14,
  lineHeight: 1.6,
  paddingHorizontal: 16,
  paddingVertical: 12,
  indentType: "spaces",
  indentSize: 2,
  vimMode: false,
  typewriterScrolling: false,
  showIndentGuides: true,
  bracketColorization: true,
  markdownListIndent: true,
  listHangingIndent: true,
  showStatusBar: true,
};

設定から Extension へのマッピング

各設定は1つ以上の CodeMirror Extension または設定オプションにマッピングされます。Extension の配列は現在の設定に基づいて動的に構築されます。

import { EditorState, Compartment } from "@codemirror/state";
import { EditorView, keymap, lineNumbers, scrollPastEnd } from "@codemirror/view";
import { indentUnit } from "@codemirror/language";
import { indentWithTab } from "@codemirror/commands";
import { vim } from "@replit/codemirror-vim";

function buildExtensions(settings: EditorSettings) {
  return [
    // Vim モード(条件付き)
    ...(settings.vimMode ? [vim()] : []),

    // テーマによるフォントとレイアウト
    EditorView.theme({
      ".cm-scroller": {
        fontFamily: `"${settings.fontFamily}", monospace`,
        fontSize: `${settings.fontSize}px`,
        lineHeight: String(settings.lineHeight),
      },
      ".cm-content": {
        padding: `${settings.paddingVertical}px 0`,
      },
      ".cm-line": {
        paddingLeft: `${settings.paddingHorizontal}px`,
        paddingRight: `${settings.paddingHorizontal}px`,
      },
    }),

    // インデント
    EditorState.tabSize.of(settings.indentSize),
    indentUnit.of(
      settings.indentType === "tab"
        ? "\t"
        : " ".repeat(settings.indentSize),
    ),
    keymap.of([indentWithTab]),

    // 機能トグル
    ...(settings.markdownListIndent ? [markdownListIndent()] : []),
    ...(settings.listHangingIndent ? [listHangingIndent()] : []),
    ...(settings.showIndentGuides ? [indentGuides()] : []),
    ...(settings.typewriterScrolling ? [typewriterScrolling()] : []),
    ...(settings.bracketColorization ? [bracketColorize()] : []),

    scrollPastEnd(),
  ];
}

フォント設定

フォントファミリー、サイズ、行の高さ、パディングはすべて EditorView.theme() で適用されます。CodeMirror はカーソル位置決め、行高さの計算、ビューポート測定に正確なピクセル値が必要なため、これらの値を CSS カスタムプロパティとして設定することはできません。

インデント

2つの CodeMirror プリミティブがインデントを制御します。

  • EditorState.tabSize.of(n) — タブ文字の視覚的な幅を設定
  • indentUnit.of(unit) — ユーザーが Tab を押したときに挿入される内容を設定。"\t" 文字列はタブ文字を挿入し、スペースの文字列はそのスペース数を挿入

機能トグル

オプションの Extension は三項演算子とスプレッド演算子を使って条件付きで含めます。

...(settings.typewriterScrolling ? [typewriterScrolling()] : []),

このパターンは明確で、ネストされた if-else ブロックを回避します。各トグルは対応する Extension を追加または省略します。

ランタイム再設定のためのコンパートメント

一部の設定はエディターを再作成せずにランタイムで変更できます。CodeMirror の Compartment がこれを可能にします。

const readOnlyCompartment = new Compartment();

const extensions = [
  readOnlyCompartment.of(EditorState.readOnly.of(false)),
  // ... other extensions
];

// 後で readOnly をトグル:
view.dispatch({
  effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(true)),
});

コンパートメントは頻繁に変更される設定(分割ペインでの読み取り専用モードなど)に便利です。まれにしか変更されない設定(フォントサイズや Vim モードなど)では、エディターの再作成のほうがシンプルで、多数のコンパートメントを管理する複雑さを回避できます。

💡 Tip

実用的なガイドライン: アクティブな編集中に変更される設定(読み取り専用トグル、行の折り返し)にはコンパートメントを使用します。設定ダイアログで変更される設定(フォント、インデント、機能トグル)にはエディターを再作成します。CodeMirror は高速な初期化を前提に設計されているため、EditorView の再作成コストは低いです。

バリデーションとデフォルト

共有スキーマパッケージが設定をバリデーションし、デフォルトを適用します。

function validateEditorSettings(input: Partial<EditorSettings>): EditorSettings {
  return {
    fontFamily: input.fontFamily || defaultEditorSettings.fontFamily,
    fontSize: clamp(input.fontSize ?? defaultEditorSettings.fontSize, 10, 24),
    lineHeight: input.lineHeight ?? defaultEditorSettings.lineHeight,
    paddingHorizontal: clamp(
      input.paddingHorizontal ?? defaultEditorSettings.paddingHorizontal,
      0,
      48,
    ),
    paddingVertical: clamp(
      input.paddingVertical ?? defaultEditorSettings.paddingVertical,
      0,
      48,
    ),
    indentType: input.indentType === "tab" ? "tab" : "spaces",
    indentSize: clamp(input.indentSize ?? defaultEditorSettings.indentSize, 1, 8),
    vimMode: input.vimMode ?? defaultEditorSettings.vimMode,
    typewriterScrolling: input.typewriterScrolling ?? defaultEditorSettings.typewriterScrolling,
    showIndentGuides: input.showIndentGuides ?? defaultEditorSettings.showIndentGuides,
    bracketColorization: input.bracketColorization ?? defaultEditorSettings.bracketColorization,
    markdownListIndent: input.markdownListIndent ?? defaultEditorSettings.markdownListIndent,
    listHangingIndent: input.listHangingIndent ?? defaultEditorSettings.listHangingIndent,
    showStatusBar: input.showStatusBar ?? defaultEditorSettings.showStatusBar,
  };
}

function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

このバリデーションにより、ユーザーが設定ファイルに何を入力しても、エディターは常に期待される範囲内の安全な値を受け取ります。同じバリデーション関数がバックエンド(ディスクから設定を読み取る際)とフロントエンド(CodeMirror に渡す前)の両方で使用されます。

React 統合パターン

React アプリケーションでは、設定が変更されると useEffect の依存関係を通じてエディターが再作成されます。

useEffect(() => {
  if (!containerRef.current || !settings) return;

  const extensions = buildExtensions(settings);
  const state = EditorState.create({ doc: content, extensions });
  const view = new EditorView({ state, parent: containerRef.current });

  return () => view.destroy();
}, [settings]);

settings オブジェクトはメモ化され、実際の値が変更された場合にのみ effect が再実行されます。

const settings = useMemo(
  () => appSettings
    ? { ...defaultEditorSettings, ...appSettings.editor }
    : null,
  [appSettings],
);

📝 Note

エディターが再作成される際は、ドキュメントの内容とスクロール位置を保持する必要があります。content プロパティが現在のドキュメントを提供し、スクロール位置は view.scrollDOM.scrollTop で保存・復元できます。

設定 UI

各設定は設定 UI のフォームコントロールに対応します。設定ページは現在の設定とマージされる部分的な更新をディスパッチします。

interface EditorSettingsProps {
  values: EditorSettings;
  onChange: (values: Partial<EditorSettings>) => void;
}

// 例: フォントサイズ入力
<input
  type="number"
  min={10}
  max={24}
  value={values.fontSize}
  onChange={(e) => onChange({ fontSize: Number(e.target.value) })}
/>

// 例: 機能トグル
<input
  type="checkbox"
  checked={values.typewriterScrolling}
  onChange={(e) => onChange({ typewriterScrolling: e.target.checked })}
/>

部分更新パターン(Partial<EditorSettings>)により、各コントロールは他の設定を知ることなく自分のフィールドのみを更新できます。

Revision History