VSCodeの解剖
VSCodeがOSSとしてソースコード公開されているから学んでみたい
みんな大好きVSCode。JavaScriptで作成されてブラウザ(monaco-editor)や3大OSのデスクトップ(vscode)で利用することができる。
そんなVSCodeが確か2015年あたりにオープンソース化されてGitHubに公開されてあるので、せっかくなんで覗いてみる。
VSCode自体はTypeScriptで構築されておりブラウザ(monaco-editor)はそこから直接パッケージ化され、デスクトップ版はelectronを使ってパッケージ化される
目標はVSCode Editor上でのMiniMapの実装のノウハウを理解すること(どこかで使いたいから)。
各構成要素
このページによると画面上のパーツには名前がついている
あと名前がついているやつ列挙
- MiniMap
- Breadcrumbs
- Explorer(ファイル一覧)
話それるけどドキュメント読んで発見したやつ
ポータブルモード
ってのがあるみたい。usbに入れて持ち運べるんだって。すごいね。
Portable mode#
Visual Studio Code supports Portable mode installation. This mode enables all data created and maintained by VS Code to live near itself, so it can be moved around across environments, for example, on a USB drive. See the VS Code Portable Mode documentation for details.
エクスプローラー上で2つのファイルを選択し右クリックからファイル比較が行える
禅モードなるものがある
zen mode
スクリーンキャストモードなるものがある
画面上に入力したキーを表示してくれるあれ
もぐりこんでみる
いろいろ巡回した結果おそらく次のような構造になっていると思っている
src/vs/editor
がコア機能でmonaco-editorはこれで動いている
editorとはEditor Groupの部分
src/vs/workbench
がサイドバーやステータスバーやパネルなど統合的な部分
つまりMiniMapだけみたいのであればeditorのみを探れば良さげ
なぜそう判断したかというとtsconfig.monaco.jsonが次のような構成ファイルになっているから
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"types": ["trusted-types"],
"paths": {},
"module": "amd",
"moduleResolution": "classic",
"removeComments": false,
"preserveConstEnums": true,
"target": "es6",
"sourceMap": false,
"declaration": true
},
"include": [
"typings/require.d.ts",
"typings/thenable.d.ts",
"vs/css.d.ts",
"vs/monaco.d.ts",
"vs/nls.d.ts",
"vs/editor/*",
"vs/base/common/*",
"vs/base/browser/*",
"vs/platform/*/common/*",
"vs/platform/*/browser/*"
],
"exclude": [
"node_modules/*",
"vs/platform/files/browser/htmlFileSystemProvider.ts"
]
}
monacoのbuildを覗いてみてもworkbenchは入っていない。しかしサイドバーの設定などはworkbenchファイルにちょこちょこ見かける
build部分複雑すぎて全然わからない
最初のコミットログから追えばわかるんじゃないかと思ったけど、どうやら最初のコミット(8f35cc47683)はOSS化したあとのプロダクトコミットみたい。つまりOSS以前の作り方の流れがよめない。しかし、2015年からの6年間で8万コミットってすごいな
多分だけど
masterブランチ発火でazure-pipelineが走るのかな
gulpに多数のtaskが登録されてあって、各パイプラインの工程で
windows
mac(darwin)
linux(web含む)
がそれぞれBuildされPublishされる。
各工程の詳細はproduct-build-*.yml
の乗っている
Buildの工程でgulpのタスクを発火させる
gulpfile.vscode.web.js
みるとtask自体がなにもしないようになってる。なんで?
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
const gulp = require('gulp');
const noop = () => { return Promise.resolve(); };
gulp.task('minify-vscode-web', noop);
gulp.task('vscode-web', noop);
gulp.task('vscode-web-min', noop);
gulp.task('vscode-web-ci', noop);
gulp.task('vscode-web-min-ci', noop);
2年前のコミットで次のように書かれてた
web - get product config through build and not via API
web版はビルド時に設定がされる、APIによってではないと。ふむつまりどういうこと?
わかったかも。多分azure-pipelineではbuildよりもcompileが先。compile時にjs上のすべての環境変数は埋め込まれているんだろう。
確かによく考えたら、どのdesktop環境でも同じソースコード使うんだし、compileは共通にしたほうがよさそう。
ここでproduct-compile.yml
でコンパイルが走ってる
- script: |
set -e
yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check
displayName: Compile & Hygien
多分 core-ci と extensions-ci がコンパイルであとは静的解析かな
みたところmonacoのpublishとazurue-pipelineによるvscodeのpublishは別でやるみたい。つまり vscodeにはmonaco-editorは関係なく、ソースコードが同じだけみたい。
build/monaco/README.MD
# Steps to publish a new version of monaco-editor-core
## Generate monaco.d.ts
* The `monaco.d.ts` is now automatically generated when running `gulp watch`
## Bump version
* increase version in `build/monaco/package.json`
## Generate npm contents for monaco-editor-core
* Be sure to have all changes committed **and pushed to the remote**
* (the generated files contain the HEAD sha and that should be available on the remote)
* run gulp editor-distro
## Publish
* `cd out-monaco-editor-core`
* `npm publish`
ここでgulp editor-distro しているが、その中身はgulpfile.editor.jsにあり、内容はmonaco用のコンパイル。
解析のやり方がわかってきた
gulpfileのtaskが定義されている部分をみる。その呼出元を見る。のくり返し。
gulefileたち
gulpfile.compile.js
gulpfile.editor.js
gulpfile.extensions.js
gulpfile.hygiene.js
gulefile.reh.js
gulefile.vscode.js
gulpfile.vscode.linux.js
gulpfile.vscode.web.js
gulpfile.vscode.win32.js
gulpfile.compile.js はコンパイルタスクが定義されている。npm scriptsからも呼べるが、gulpfile.vscode.jsでcore-ciとして定義されているタスクからも呼ばれる。core-ciタスクはazure-pipelineのコンパイルステージにて実行される。
gulpfile.vscode.linux.js
gulpfile.vscode.web.js
gulpfile.vscode.win32.js
はそれぞれazure-pipelineのBuildやPublishなどのフェーズで活用される。
とりあえず gulpfile.compile.jsの中身をのぞけばwebベースのvscodeがみれるはず。
のぞいてみたところコンパイルはどうやら/build/lib/compilation
ファイルのcompileTaskが担っているみたい
coreとextensionの関係性がよくわからない
rimrafって単語がちらほら出てきてる。どうやらビルド前に出力先ディレクトリをきれいにするやつみたい
いよいよ本命の /build/lib/compilation
をのぞいてみる
export function compileTask(src: string, out: string, build: boolean): () => NodeJS.ReadWriteStream {
return function () {
if (os.totalmem() < 4_000_000_000) {
throw new Error('compilation requires 4GB of RAM');
}
const compile = createCompile(src, build, true);
const srcPipe = gulp.src(`${src}/**`, { base: `${src}` });
let generator = new MonacoGenerator(false);
if (src === 'src') {
generator.execute();
}
return srcPipe
.pipe(generator.stream)
.pipe(compile())
.pipe(gulp.dest(out));
};
}
osにtotalmemというAPIがあったとは。あとコンパイルに4G必要とは。パイプラインに対する要求スペックが高い。こんなものなのかな?あとはcreateCompile
とMonacoGeneratorなるものを解析すればわかりそう
にしてもgulpだらけだ。
build/lib/compilation
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as es from 'event-stream';
import * as fs from 'fs';
import * as gulp from 'gulp';
import * as path from 'path';
import * as monacodts from './monaco-api';
import * as nls from './nls';
import { createReporter } from './reporter';
import * as util from './util';
import * as fancyLog from 'fancy-log';
import * as ansiColors from 'ansi-colors';
import * as os from 'os';
import ts = require('typescript');
const watch = require('./watch');
const reporter = createReporter();
function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions {
const rootDir = path.join(__dirname, `../../${src}`);
let options: ts.CompilerOptions = {};
options.verbose = false;
options.sourceMap = true;
if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry
options.sourceMap = false;
}
options.rootDir = rootDir;
options.baseUrl = rootDir;
options.sourceRoot = util.toFileUri(rootDir);
options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1;
return options;
}
function createCompile(src: string, build: boolean, emitError?: boolean) {
const tsb = require('gulp-tsb') as typeof import('gulp-tsb');
const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps');
const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json');
const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) };
if (!build) {
overrideOptions.inlineSourceMap = true;
}
const compilation = tsb.create(projectPath, overrideOptions, false, err => reporter(err));
function pipeline(token?: util.ICancellationToken) {
const bom = require('gulp-bom') as typeof import('gulp-bom');
const utf8Filter = util.filter(data => /(\/|\\)test(\/|\\).*utf8/.test(data.path));
const tsFilter = util.filter(data => /\.ts$/.test(data.path));
const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path)));
const input = es.through();
const output = input
.pipe(utf8Filter)
.pipe(bom()) // this is required to preserve BOM in test files that loose it otherwise
.pipe(utf8Filter.restore)
.pipe(tsFilter)
.pipe(util.loadSourcemaps())
.pipe(compilation(token))
.pipe(noDeclarationsFilter)
.pipe(build ? nls.nls() : es.through())
.pipe(noDeclarationsFilter.restore)
.pipe(sourcemaps.write('.', {
addComment: false,
includeContent: !!build,
sourceRoot: overrideOptions.sourceRoot
}))
.pipe(tsFilter.restore)
.pipe(reporter.end(!!emitError));
return es.duplex(input, output);
}
pipeline.tsProjectSrc = () => {
return compilation.src({ base: src });
};
return pipeline;
}
export function compileTask(src: string, out: string, build: boolean): () => NodeJS.ReadWriteStream {
return function () {
if (os.totalmem() < 4_000_000_000) {
throw new Error('compilation requires 4GB of RAM');
}
const compile = createCompile(src, build, true);
const srcPipe = gulp.src(`${src}/**`, { base: `${src}` });
let generator = new MonacoGenerator(false);
if (src === 'src') {
generator.execute();
}
return srcPipe
.pipe(generator.stream)
.pipe(compile())
.pipe(gulp.dest(out));
};
}
export function watchTask(out: string, build: boolean): () => NodeJS.ReadWriteStream {
return function () {
const compile = createCompile('src', build);
const src = gulp.src('src/**', { base: 'src' });
const watchSrc = watch('src/**', { base: 'src', readDelay: 200 });
let generator = new MonacoGenerator(true);
generator.execute();
return watchSrc
.pipe(generator.stream)
.pipe(util.incremental(compile, src, true))
.pipe(gulp.dest(out));
};
}
const REPO_SRC_FOLDER = path.join(__dirname, '../../src');
class MonacoGenerator {
private readonly _isWatch: boolean;
public readonly stream: NodeJS.ReadWriteStream;
private readonly _watchedFiles: { [filePath: string]: boolean; };
private readonly _fsProvider: monacodts.FSProvider;
private readonly _declarationResolver: monacodts.DeclarationResolver;
constructor(isWatch: boolean) {
this._isWatch = isWatch;
this.stream = es.through();
this._watchedFiles = {};
let onWillReadFile = (moduleId: string, filePath: string) => {
if (!this._isWatch) {
return;
}
if (this._watchedFiles[filePath]) {
return;
}
this._watchedFiles[filePath] = true;
fs.watchFile(filePath, () => {
this._declarationResolver.invalidateCache(moduleId);
this._executeSoon();
});
};
this._fsProvider = new class extends monacodts.FSProvider {
public readFileSync(moduleId: string, filePath: string): Buffer {
onWillReadFile(moduleId, filePath);
return super.readFileSync(moduleId, filePath);
}
};
this._declarationResolver = new monacodts.DeclarationResolver(this._fsProvider);
if (this._isWatch) {
fs.watchFile(monacodts.RECIPE_PATH, () => {
this._executeSoon();
});
}
}
private _executeSoonTimer: NodeJS.Timer | null = null;
private _executeSoon(): void {
if (this._executeSoonTimer !== null) {
clearTimeout(this._executeSoonTimer);
this._executeSoonTimer = null;
}
this._executeSoonTimer = setTimeout(() => {
this._executeSoonTimer = null;
this.execute();
}, 20);
}
private _run(): monacodts.IMonacoDeclarationResult | null {
let r = monacodts.run3(this._declarationResolver);
if (!r && !this._isWatch) {
// The build must always be able to generate the monaco.d.ts
throw new Error(`monaco.d.ts generation error - Cannot continue`);
}
return r;
}
private _log(message: any, ...rest: any[]): void {
fancyLog(ansiColors.cyan('[monaco.d.ts]'), message, ...rest);
}
public execute(): void {
const startTime = Date.now();
const result = this._run();
if (!result) {
// nothing really changed
return;
}
if (result.isTheSame) {
return;
}
fs.writeFileSync(result.filePath, result.content);
fs.writeFileSync(path.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums);
this._log(`monaco.d.ts is changed - total time took ${Date.now() - startTime} ms`);
if (!this._isWatch) {
this.stream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.');
}
}
}
とりあえず out-build
というパスにコンパイルされたものが置かれることがわかった。
gulpfile.vscode.jsをみてみると
out-build をもとにminifyしてるみたい。その結果がout-vscode-minフォルダにはいってる?
vscodeのローカルbuildのしかたのってた
ソースコードもってきて
ライブラリをインストールし
yarn watch
でbuildする
デスクトップ版(Linux, Mac)は/scripts/code.sh
で起動する。cli上だとcode-cli.shのほう。
デスクトップ版(WIndows)は/scripts/code.bat
で起動する。cli上だとcode-cli.batのほう。
Web版はyarn web
で起動する