React による Monaco Editor 統合と高度な設定:本格的なコードエディタ構築

Monaco Editor は、Visual Studio Code のコアエンジンとして知られる高機能コードエディタです。このエディタを React アプリケーションにオープンソースで取り入れるためのラッパーとして、react-monaco-editor が広く利用されています。本稿では、このライブラリを活用して、本格的なコード編集機能を備えた UI を実装するための手順と実践的チューニング方法を解説します。

導入準備と基本構成

まず、プロジェクトにライブラリを追加します。

npm install react-monaco-editor monaco-editor
# または
yarn add react-monaco-editor monaco-editor

次に、编辑器コンポーネントを簡易実装します。ステート管理とエディタ初期化ロジックを分離し、再利用可能な設計としています。

import React, { useState, useCallback } from 'react';
import Editor from 'react-monaco-editor';

const CodeEditor = () => {
  const [source, setSource] = useState('// 対応している言語: JavaScript, TypeScript, JSON, CSS など');

  const handleUpdate = useCallback((value) => {
    setSource(value || '');
  }, []);

  const config = {
    automaticLayout: true,
    fontSize: 14,
    lineNumbers: 'on',
    minimap: { enabled: true, maxColumn: 80 },
    tabSize: 2,
    theme: 'vs-dark',
    wordWrap: 'on'
  };

  return (
    <div style={{ height: '500px' }}>
      <Editor
        value={source}
        language="typescript"
        options={config}
        onChange={handleUpdate}
        editorDidMount={(editor) => {
          editor.focus();
        }}
      />
    </div>
  );
};

export default CodeEditor;

ビルド시스템調整: Webpack プラグインの統合

Monaco Editor は Webpack の動的インポートに依存しているため、ビルド設定での特別扱いが必要です。Monaco 提供の Webpack Plugin を利用し、必要な言語ファイルと機能モジュールのみを含めるように最適化します。

// webpack.config.js
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

module.exports = {
  plugins: [
    new MonacoWebpackPlugin({
      languages: ['javascript', 'typescript', 'json', 'html', 'css'],
      features: ['accessibilityHelp', 'bracketMatching', 'caretOperations', 'cursorUndo', 'dnd', 'find', 'gotoLine', 'hover', 'iPadShowKeyboard', 'inPlaceReplace', 'linesOperations', 'mouseWheelScroll', 'multiCursor边际操作', 'referenceSearch', 'rename', 'smartSelect', 'suggest', 'toggleTabFocusMode', 'transpose']
    })
  ],
  module: {
    rules: [
      {
        test: /monaco-editor/,
        type: 'asset/source'
      }
    ]
  }
};

この設定により、Unpkg による CDN 読み込みや手動でのポインタ調整を避け、静的アセットとして効率的にビルドできます。

細かなカスタマイズと高度利用

エディタのマウント完了後に得られるエディタ API を活用することで、高度なインタラクションも実現可能です。

const editorDidMount = (editor, monaco) => {
  // リジョンしたでの範囲強調表示
  const model = editor.getModel();
  monaco/editor.setModelMarkers(model, 'lint', [
    {
      severity: monaco.MarkerSeverity.Error,
      startLineNumber: 3,
      startColumn: 11,
      endLineNumber: 3,
      endColumn: 25,
      message: 'Identifier ' + JSON.stringify('unusedVar') + ' is declared but never used.',
      code: 'TS6133'
    }
  ]);

  // カスタムコマンド追加
  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
    console.log('Save proxy called');
    // 実際の保存処理は別途実装
  });

  // カスタム言語定義例: ミニ言語のシンタックスハイライト
  monaco.languages.register({ id: 'domainLang' });
  monaco.languages.setMonarchTokensProvider('domainLang', {
    tokenizer: {
      root: [
        [/[a-z_$][\w$]*/, { cases: { '@keywords': 'keyword', '@default': 'identifier' } }],
        [/[0-9]+/, 'number'],
        [/["'`]/, { token: 'string', bracket: '@open', next: '@string' }]
      ],
      string: [
        [/[^"'\`\\]+/, 'string'],
        [/\\./, 'string.escape'],
        [/["'`]/, { token: 'string', bracket: '@close', next: '@pop' }]
      ]
    },
    keywords: ['exec', 'eval', 'query']
  });
};

差分編集と同時編集処理

2つのテキスト比較には MonacoDiffEditor を利用します。元の内容と変更後の内容を引数で渡すだけで、前述の設定と同様のオプションを受け取ります。

import { DiffEditor } from 'react-monaco-editor';

const ComparisonViewer = () => {
  const before = [
    'function greet(name) {',
    '  console.log("Hello, " + name);',
    '}'
  ].join('\n');

  const after = [
    'function greet(name) {',
    '  console.log(`Hello, ${name.toUpperCase()}`);',
    '}'
  ].join('\n');

  const options = {
    renderSideBySide: true,
    readOnly: true,
    automaticLayout: true,
    diffWordWrap: 'on'
  };

  return (
    <div style={{ height: '400px' }}>
      <DiffEditor
        language="javascript"
        original={before}
        modified={after}
        options={options}
      />
    </div>
  );
};

このモジュールは、Git ブラウザベースの変更.diff 表示や CI 上での自動レビュー結果画面など、ソフトウェア開発支援ツールに有効です。

パフォーマンスとメモリ制御

  • 初期描画時に全言語定義を読み込まないよう、初期化時に必要言語のみをロードする設定が推奨(例: languages: ['typescript']
  • 多くのエディタを一度に描画する際は、 эмулятор要素の static 表示の抑制注意(bias があるとレイアウト崩れの原因に)
  • editorWillUnmount 回呼で、非同期リスナやエディタされた履歴データの解放忘れに注意する
  • 編集対象が histórico 化された大きなファイルは、別途ストリーミング取得や lazy-load を検討

補完の高度な拡張

補完候補は registerCompletionItemProvider で独自実装可能です。特に、言語固有の API 名・構文補完や、ランタイムバインドされた DTO フィールド補完を実現できます。

monaco.languages.registerCompletionItemProvider('typescript', {
  provideCompletionItems: (model, position) => {
    const word = model.getWordUntilPosition(position);
    const range = {
      startLineNumber: position.lineNumber,
      endLineNumber: position.lineNumber,
      startColumn: word.startColumn,
      endColumn: word.endColumn
    };

    return {
      suggestions: [
        {
          label: 'sampleSnippet',
          kind: monaco.languages.CompletionItemKind.Snippet,
          insertText: 'console.log(${1:message});',
          insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
          range
        }
      ]
    };
  }
});

上記のように snippet 機能と連携すると、定型コードの生成に活用できます。VS Code の Code Action やQuickFix は独自プロバイダで再現も可能ですが、多くの場合 Monaco のビルトイン機能で十分です。

タグ: monaco-editor React webpack integration TypeScript

5月20日 12:19 投稿