Help us understand the problem. What is going on with this article?

Monaco Editor をハックする

Monaco Editor をハックする

by mizchi
1 / 34
  • for ginza.js#7
  • @mizchi
  • このスライドツールの作者
  • 今はワケあってこの会場の会社にいる(Plaid)

お品書き

  1. VSCode をハックしようとして敗北した
  2. MonacoEditor をハックしてる

フロントエンドの仕事、ブラウザだけで完結するはず、と思ったことないですか?


フロントエンドのおしごと

  • コンポーネントを作る
  • ビルドする
  • テストする
  • ブラウザで確認する

フロントエンドのおしごと

  • コンポーネントを作る(ブラウザでできる?)
  • ビルドする(ブラウザでできる?)
  • テストする(ブラウザでできる)
  • ブラウザで確認する(ブラウザでできる)

経緯: コンパイラを作った

フロントエンドでフロントエンドをビルドする で、ブラウザ上で動くコンパイラ作った

今日は、このコンパイラを動かすためのエディタを作ってる話


@mizchi/web-compiler (仮)ができること

https://github.com/mizchi/web-compiler

サンプルコード

// ブラウザで実行
import { compile } from "@mizchi/web-compiler";

// package.json
const pkg = {
  dependencies: {
    "lodash.flatten": "*"
  }
};

// tsconfig.json
const tsConfig = `{
  compilerOptions: {
    target: "es5"
  }
}`

// コード
const index = `
import foo from "./foo";
import flatten from "lodash.flatten";
console.log(flatten([[1], 2]), foo);
`;
const foo = `export default 1;`
const files = {
  "/index.js": index,
  "/foo.js": foo
},

// /index.js でビルド
const compiledCode = await compile({
  entry: "/index.js",
  pkg, tsConfig, files
});
  • インメモリの仮想 FS で相対パスを解決
  • npm のモジュールを解決
  • 任意の tsconfig.json の設定で typescript をコンパイル
  • terser で minify

モダンなエディタに要求されることは?


答え: TypeScript の補完ができる


エディタをどう作るか

// このインメモリなファイルを TypeScript で補完して快適に編集したい
const files: {[filepath: string]: string} = {...}

=> とりあえず VSCode ベースでやる


エディタの設計: 3 つの方針

  1. VSCode Online 上の拡張を作る
  2. VSCode をハックする
  3. MonacoEditor をハックする

VSCode Online 使ってみた

Visual Studio Online - Web ベースの IDE と共同コード エディター

  • MS Account 登録
  • Environment(ホストとなる仮想マシン) の登録・起動
  • エディタ起動

VSCode Online 感想

  • Environment を自由に作れるけど、セットアップに手間がかかる
  • クラウドなのでキビキビ感が足りない
  • 要課金だしお手軽感がない
  • MS Account はちょっと… 電話してくるのもやめてください

手間の関係で NG


もうどうせだから vscode コード読もうぜ!


VSCode がハックできるかの調査

仮説: 「ブラウザで動く VSCode Online があるなら、ブラウザ用ビルドがあるはず…」

  • https://github.com/microsoft/vscode
  • web ビルド用のコマンドがあったので、 (yarn web) ので、それを調査する
  • どうやらリロードごとに FS が揮発してるので、そこを任意の処理(永続化)を入れられるか?という視点で調べる

コードリーディング: VSCode の基本的な設計

  • 専用の AMD Loader で起動する (github.com/microsoft/vscode-loader)
  • 最初に複数の ~Service を WebWorker のワーカーで起動する
  • サービスに任意の実装の Provider を注入することで、役割・環境ごとに実際の処理を切り分けられる
  • Service の起動が終わると、各 Extension を起動する (言語ごとのプリインの拡張機能など)
  • Service と Extension が起動し終わったら、 Workbench (VSCode の UI 部分) を起動

(UI は何のフレームワークも使ってない独自のものだが、 state の書き換えがトランザクション前提で Reducer っぽい)

ここまでのメモ: https://gist.github.com/mizchi/9dbd9dbbc97378c48ced47964dc97055


VSCode web ビルドの調査

  • yarn watch-client してから yarn web すると、ブラウザで動くビルドが起動する
  • vscode/src/vs/code/browser/workbench/workbench-dev.html が読み込まれている。
  • FileService に注入された RemoteFileSystemProvider はどことも通信しておらず、 new Map() のオンメモリキャッシュに乗ってるので、永続化されない
  • Extension を全部消したりしてみたが、 vscode.vscode-api-tests がないと立ち上がらない。どうやら ここもテスト用の Mock っぽいので、色々死にそう。


Web ビルド: わかったこと

  • 色々とパッチ当てたら動くが大変
  • もうちょっとハックしやすければ VSCode の独自ビルド路線もアリだが…
  • 今回は monaco-editor をハックする路線で頑張る

monaco-editor をハックする


monaco-editor


monaco-editor の実装

  • 言語ごとに WebWorker プロセスを立てて通信する
  • クライアント上の monaco に変更を加えると、ワーカーの mirror に同期される
  • ワーカー上で読み書きしたことを UI スレッドに書き戻すことで、双方向通信を実現

monaco-typescript

Using the Language Service API · microsoft/TypeScript Wiki


monaco-typescript 読んでわかったこと

  • monaco-typescript/tsWorker はファイルスキーマで登録された monaco.Model を解決する
  • monaco の Model オブジェクトをファイルスキーマで登録すれば補完が動きそう

=> 実現できる API を調べる


monaco で仮想 FS を構築して補完の準備

monaco 上でのファイルオブジェクトの生成

import * as monaco from "monaco-editor";
const model = monaco.editor.createModel(
  "export default { a: 1 }",
  "typescript",
  monaco.Uri.from({
    scheme: "file",
    path: "/foo.ts"
  })
);

(同名のファイルを生成するとエラーになる)

エディタへの Model 読み込み

const editor = monaco.editor.create(...);
editor.setModel(model);

今エディタで開いている TypeScript の型エラーを取得

const uri = editor.getModel().uri;
const worker = await monaco.languages.typescript.getTypeScriptWorker();
const proxy = await worker(uri);
const diagnostics = await proxy.getSemanticDiagnostics(uri.toString());
console.log(diagnostics);

モデルの破棄(削除)

model.dispose();

つまり…

こういう FS を構築して

const model = monaco.editor.createModel(
  "export default { a: 1 }",
  "typescript",
  monaco.Uri.from({
    scheme: "file",
    path: "/foo.ts"
  })
);

const model = monaco.editor.createModel(
  'import * as foo from "./foo";console.log(foo)',
  "typescript",
  monaco.Uri.from({
    scheme: "file",
    path: "/index.ts"
  })
);

/*
/index.ts
/foo.ts
*/

index.ts から ./foo を引くと補完できる!!


デモ

https://relaxed-franklin-8384b4.netlify.com/


TypeScript のコンパイラオプションを変更する

import { parseConfigFileTextToJson } from "typescript";
const conf = parseConfigFileTextToJson("/tsconfig.json", '{ "compilerOptions": {} }');
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
  conf.config.compilerOptions
);

Thx @L_e_k_o


外部の型定義ファイルを読み込む

monaco.languages.typescript.typescriptDefaults.addExtraLib(
  "declare module '*';",
  "decls.d.ts"
);

まだできてないこと

  • npm の @types からの型定義の読み込み
  • *.d.ts の型定義ファイルのダウンロード(その中からさらに相対パスがあり難しい

ツラミ

  • ブラウザにろくな永続層がない
  • NativeFileSystem API に必要な仕様が足りない
  • ./foo./foo.js/foo.json./foo/index.jsfoo/package.json#main なのか一意に決まらないものの、ネットワーク経由で解決するのが非効率
  • Filer みたいな基本的なもので、作るものが多い!

https://mizchi.hatenablog.com/entry/2019/09/08/090057


おまけ: react-unite


今後

  • VSCode もう一回読む
  • コンパイラのパターン増やしてちゃんと作る
  • TSの型解決を頑張る

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away