Closed15

VSCodeの解剖

ハトすけハトすけ

VSCodeがOSSとしてソースコード公開されているから学んでみたい

みんな大好きVSCode。JavaScriptで作成されてブラウザ(monaco-editor)や3大OSのデスクトップ(vscode)で利用することができる。

そんなVSCodeが確か2015年あたりにオープンソース化されてGitHubに公開されてあるので、せっかくなんで覗いてみる。
https://github.com/microsoft/vscode

VSCode自体はTypeScriptで構築されておりブラウザ(monaco-editor)はそこから直接パッケージ化され、デスクトップ版はelectronを使ってパッケージ化される

目標はVSCode Editor上でのMiniMapの実装のノウハウを理解すること(どこかで使いたいから)。

ハトすけハトすけ

話それるけどドキュメント読んで発見したやつ

ポータブルモード

ってのがあるみたい。usbに入れて持ち運べるんだって。すごいね。

https://code.visualstudio.com/docs/setup/setup-overview

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
https://blog.triana.jp/?p=7090

スクリーンキャストモードなるものがある

画面上に入力したキーを表示してくれるあれ
https://kakakakakku.hatenablog.com/entry/2020/06/09/084655

ハトすけハトすけ

もぐりこんでみる

いろいろ巡回した結果おそらく次のような構造になっていると思っている

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が担っているみたい

ハトすけハトすけ

いよいよ本命の /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のしかたのってた
https://github.com/Microsoft/vscode/wiki/How-to-Contribute#build-and-run-from-source

ソースコードもってきて
ライブラリをインストールし
yarn watchでbuildする
デスクトップ版(Linux, Mac)は/scripts/code.shで起動する。cli上だとcode-cli.shのほう。
デスクトップ版(WIndows)は/scripts/code.batで起動する。cli上だとcode-cli.batのほう。
Web版はyarn webで起動する

このスクラップは2021/12/01にクローズされました