🔥

Tauri(Rust + React + TypeScript) から始めるディスクトップアプリ #2[MonacoEditor導入]

2023/08/08に公開

はじめに

今回は、monaco Editorの組み込みをします。
前も書いたのですが、理由は下記の通りです。

Editorを自作するのは時間がいくらあっても足りないと思いますし、努力に見合うリターンが得られると思いません。のでライブラリを探します。
有償のものから、MITライセンスのものまで探すとたくさんあります。
今回は、商用利用可能なライブラリで、かつ無償なライブラリを採用したいと思います。
WYSIWYGのEditorが多く検索に引っ掛かりますが、SQLを中心に記述するEditorにしたいため、WYSIWYGは不要です。時間をかけて検索し実績とサンプルの多さでAceEditorとMonacoEditorあたりが候補なりました。メリデメとか見てもさっぱりわからないので、見た目の好みを優先し、みんな大好きVisualStadioCodeで利用されているMonacoEditorを採用します。

2023/8/8 時点最新の MonacoEditor v0.41.0を利用します。
https://microsoft.github.io/monaco-editor/

MonacoEditor の組み込み

インストールします。

npm install monaco-editor@0.41.0

monaco react で検索すると様々なパッケージがあります。選定してもいいのですが、バグなど放置されている傾向があるので、今回は避けています。
monaco-editorパッケージだけで特に面倒なく利用可能です。

package.json
{
  "name": "myAPP",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "tauri": "tauri"
  },
  "dependencies": {
    "monaco-editor": "^0.41.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@tauri-apps/cli": "^1.4.0",
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}

MonacoEditor のサンプルコード

GTP様に伺っても微妙な回答しか得られなかったです。一番参考になったのは、
https://github.com/microsoft/monaco-editor
のサンプルですね。vite-react を参考にしました。
(微妙に記述が古いんであれですがw)
https://github.com/microsoft/monaco-editor/tree/main/samples

main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Editor } from "./components/Editor";
import "./useWorker.ts";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <>
      <h2>MonacoEdit</h2>
      <Editor />
    </>
  </React.StrictMode>
);

Monaco Editorは多くの言語に対応するワーカーモジュールがあり、その組み込みです。

useWorker.ts
import * as monaco from "monaco-editor";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === "json") {
      return new jsonWorker();
    }
    if (label === "css" || label === "scss" || label === "less") {
      return new cssWorker();
    }
    if (label === "html" || label === "handlebars" || label === "razor") {
      return new htmlWorker();
    }
    if (label === "typescript" || label === "javascript") {
      return new tsWorker();
    }
    return new editorWorker();
  },
};

monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);

コンポーネント

components/Editor
import { useRef, useState, useEffect, FC } from "react";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
import styles from "./Editor.module.css";

export const Editor: FC = () => {
  const [editor, setEditor] =
    useState<monaco.editor.IStandaloneCodeEditor | null>(null);
  const monacoEl = useRef(null);

  useEffect(() => {
    if (monacoEl) {
      setEditor((editor) => {
        if (editor) return editor;

        return monaco.editor.create(monacoEl.current!, {
          value: ["function x() {", '\tconsole.log("Hello world!");', "}"].join(
            "\n"
          ),
          language: "typescript",
          lineNumbers: "on", // テキストエディタの行番号を表示するかどうか
          roundedSelection: true, // 選択範囲を角丸にするかどうか
          scrollBeyondLastLine: false, // テキストエディタの最後の行を超えてスクロールするかどうか
          readOnly: false, // テキストエディタを読み取り専用にするかどうか
          theme: "vs-dark", // テキストエディタのテーマ
        });
      });
    }

    return () => editor?.dispose();
  }, [monacoEl.current]);

  return <div className={styles.Editor} ref={monacoEl}></div>;
};

下記にESLINTのワーニングが出ると思いますが、自分は消しちゃいました。
[monacoEl.current]

rules: {
    // 確認をOFFにする。
    "react-hooks/exhaustive-deps": "off",
  },
components/Editor.module.css
.Editor {
	width: 100vw;
	height: 100vh;
}

実行結果

npm run tauri dev

お疲れさまでした。

あとがき

ファイルにSAVEなど、次回実装しましょう。

Discussion