Monaco Editor をハックする

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

---

## お品書き

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

---

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

---

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

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

---

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

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

---

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

[フロントエンドでフロントエンドをビルドする](https://tech.plaid.co.jp/build-frontend-on-frontend/) で、ブラウザ上で動くコンパイラ作った

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

---

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

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

サンプルコード

```js
// ブラウザで実行
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 の補完ができる

---

## エディタをどう作るか

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

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

---

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

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

---

## VSCode Online 使ってみた

[Visual Studio Online \- Web ベースの IDE と共同コード エディター](https://visualstudio.microsoft.com/ja/services/visual-studio-online/)

- 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 っぽいので、色々死にそう。

![](https://i.gyazo.com/4a1778f6eca773feef70bd5b7330be69.png)

---

## Web ビルド: わかったこと

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

---

# monaco-editor をハックする

---

## monaco-editor

- https://github.com/Microsoft/monaco-editor
- VSCode のエディタ部分
- TypeScript や各言語の補完機能なども monaco で行われている
- Electron 非依存

![](https://gyazo.com/30f9725bc738dc3dfb28d3d4704a59c6.png)

---

## monaco-editor の実装

- 言語ごとに WebWorker プロセスを立てて通信する
  - そのための webpack の設定が必要
  - https://github.com/microsoft/monaco-editor-webpack-plugin
- クライアント上の monaco に変更を加えると、ワーカーの mirror に同期される
- ワーカー上で読み書きしたことを UI スレッドに書き戻すことで、双方向通信を実現

---

## monaco-typescript

- https://github.com/Microsoft/monaco-typescript
- VSCode の TypeScript の補完などを行っているモジュール
- worker 側で typescript の LanguageService を抱えて起動する

[Using the Language Service API · microsoft/TypeScript Wiki](https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API)

---

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

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

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

---

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

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

```ts
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 読み込み

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


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

```ts
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);
```

モデルの破棄(削除)

```ts
model.dispose();
```

---

## つまり…

こういう FS を構築して

```ts
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://i.gyazo.com/6c990fa1e72de502622dddc11e89e3d6.png)

---

# デモ

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

---

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

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

Thx @L_e_k_o

---

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

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

---

## まだできてないこと

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

---

## ツラミ

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

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


---

## おまけ: react-unite

- https://github.com/mizchi/react-unite
- unity3d みたいなレイアウトシステムがほしくて作った
- CSS Grid Layout のパラメタを入力値にできる。便利

![](https://i.gyazo.com/a4781fc08dd2dd96b0db5aea61563d57.gif)

---

## 今後

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

---

## 参考

- https://microsoft.github.io/monaco-editor/playground.html
- https://github.com/Leko/type-puzzle