浏览代码

Merge pull request #65 from microsoft/custom_worker

Add support for creating a custom webworker subclass
Alexandru Dima 4 年之前
父节点
当前提交
251cec6aa4
共有 8 个文件被更改,包括 357 次插入5 次删除
  1. 24 0
      package-lock.json
  2. 1 0
      package.json
  3. 16 3
      src/monaco.contribution.ts
  4. 5 0
      src/monaco.d.ts
  5. 27 1
      src/tsWorker.ts
  6. 2 1
      src/workerManager.ts
  7. 222 0
      test/custom-worker.html
  8. 60 0
      test/custom-worker.js

+ 24 - 0
package-lock.json

@@ -4,6 +4,15 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@typescript/vfs": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.2.0.tgz",
+      "integrity": "sha512-3YhBC+iyngEHjEedSAWk9rbJHoBwa2cd4h/tzb2TXmZc2CUclTl3x5AQRKNoRqm7t+X9PGTc2q2/Dpray/O4mA==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.1"
+      }
+    },
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@@ -16,6 +25,15 @@
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
       "dev": true
     },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "dev": true,
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
     "monaco-editor-core": {
       "version": "0.20.0",
       "resolved": "https://registry.npmjs.org/monaco-editor-core/-/monaco-editor-core-0.20.0.tgz",
@@ -45,6 +63,12 @@
         }
       }
     },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
     "requirejs": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "url": "https://github.com/Microsoft/monaco-typescript/issues"
   },
   "devDependencies": {
+    "@typescript/vfs": "^1.2.0",
     "monaco-editor-core": "^0.20.0",
     "monaco-languages": "^1.10.0",
     "monaco-plugin-helpers": "^1.0.2",

+ 16 - 3
src/monaco.contribution.ts

@@ -31,13 +31,15 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript.
 	private _eagerModelSync: boolean;
 	private _compilerOptions!: monaco.languages.typescript.CompilerOptions;
 	private _diagnosticsOptions!: monaco.languages.typescript.DiagnosticsOptions;
+	private _workerOptions!: monaco.languages.typescript.WorkerOptions;
 	private _onDidExtraLibsChangeTimeout: number;
 
-	constructor(compilerOptions: monaco.languages.typescript.CompilerOptions, diagnosticsOptions: monaco.languages.typescript.DiagnosticsOptions) {
+	constructor(compilerOptions: monaco.languages.typescript.CompilerOptions, diagnosticsOptions: monaco.languages.typescript.DiagnosticsOptions, workerOptions: monaco.languages.typescript.WorkerOptions) {
 		this._extraLibs = Object.create(null);
 		this._eagerModelSync = false;
 		this.setCompilerOptions(compilerOptions);
 		this.setDiagnosticsOptions(diagnosticsOptions);
+		this.setWorkerOptions(workerOptions)
 		this._onDidExtraLibsChangeTimeout = -1;
 	}
 
@@ -49,6 +51,10 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript.
 		return this._onDidExtraLibsChange.event;
 	}
 
+	get workerOptions(): monaco.languages.typescript.WorkerOptions {
+		return this._workerOptions
+	}
+
 	getExtraLibs(): IExtraLibs {
 		return this._extraLibs;
 	}
@@ -142,6 +148,11 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript.
 		this._onDidChange.fire(undefined);
 	}
 
+	setWorkerOptions(options: monaco.languages.typescript.WorkerOptions): void {
+		this._workerOptions = options || Object.create(null);
+		this._onDidChange.fire(undefined);
+	}
+
 	setMaximumWorkerIdleTime(value: number): void {
 	}
 
@@ -202,11 +213,13 @@ enum ModuleResolutionKind {
 
 const typescriptDefaults = new LanguageServiceDefaultsImpl(
 	{ allowNonTsExtensions: true, target: ScriptTarget.Latest },
-	{ noSemanticValidation: false, noSyntaxValidation: false });
+	{ noSemanticValidation: false, noSyntaxValidation: false },
+	{});
 
 const javascriptDefaults = new LanguageServiceDefaultsImpl(
 	{ allowNonTsExtensions: true, allowJs: true, target: ScriptTarget.Latest },
-	{ noSemanticValidation: true, noSyntaxValidation: false });
+	{ noSemanticValidation: true, noSyntaxValidation: false },
+	{});
 
 function getTypeScriptWorker(): Promise<(...uris: monaco.Uri[]) => Promise<monaco.languages.typescript.TypeScriptWorker>> {
 	return getMode().then(mode => mode.getTypeScriptWorker());

+ 5 - 0
src/monaco.d.ts

@@ -137,6 +137,11 @@ declare module monaco.languages.typescript {
         diagnosticCodesToIgnore?: number[];
     }
 
+    export interface WorkerOptions {
+        /** A full HTTP path to a JavaScript file which adds a function `customTSWorkerFactory` to the self inside a web-worker */
+        customWorkerPath?: string;
+    }
+
     interface IExtraLib {
         content: string;
         version: number;

+ 27 - 1
src/tsWorker.ts

@@ -10,6 +10,7 @@ import { IExtraLibs } from './monaco.contribution';
 
 import IWorkerContext = monaco.worker.IWorkerContext;
 
+
 export class TypeScriptWorker implements ts.LanguageServiceHost, monaco.languages.typescript.TypeScriptWorker {
 
 	// --- model sync -----------------------
@@ -253,8 +254,33 @@ export class TypeScriptWorker implements ts.LanguageServiceHost, monaco.language
 export interface ICreateData {
 	compilerOptions: ts.CompilerOptions;
 	extraLibs: IExtraLibs;
+	customWorkerPath?: string
+}
+
+/** The shape of the factory */
+export interface CustomTSWebWorkerFactory {
+	(TSWorkerClass: typeof TypeScriptWorker, ts: typeof import("typescript"), libs: Record<string, string>): typeof TypeScriptWorker
 }
 
 export function create(ctx: IWorkerContext, createData: ICreateData): TypeScriptWorker {
-	return new TypeScriptWorker(ctx, createData);
+	let TSWorkerClass = TypeScriptWorker
+	if (createData.customWorkerPath) {
+		// @ts-ignore - This is available in a webworker
+		if (typeof importScripts === "undefined") {
+			console.warn("Monaco is not using webworkers for background tasks, and that is needed to support the customWorkerPath flag")
+		} else {
+			// @ts-ignore - This is available in a webworker
+			importScripts(createData.customWorkerPath)
+
+			// @ts-ignore - This should come from the above eval
+			const workerFactoryFunc: CustomTSWebWorkerFactory | undefined = self.customTSWorkerFactory
+			if (!workerFactoryFunc) {
+				throw new Error(`The script at ${createData.customWorkerPath} does not add customTSWorkerFactory to self`)
+			}
+
+			TSWorkerClass = workerFactoryFunc(TypeScriptWorker, ts, libFileMap)
+		}
+	}
+
+	return new TSWorkerClass(ctx, createData);
 }

+ 2 - 1
src/workerManager.ts

@@ -72,7 +72,8 @@ export class WorkerManager {
 				// passed in to the create() method
 				createData: {
 					compilerOptions: this._defaults.getCompilerOptions(),
-					extraLibs: this._defaults.getExtraLibs()
+					extraLibs: this._defaults.getExtraLibs(),
+					customWorkerPath: this._defaults.workerOptions.customWorkerPath
 				}
 			});
 

+ 222 - 0
test/custom-worker.html

@@ -0,0 +1,222 @@
+<!--
+  To test this file, you need to use a local server. The recommendation is that you run:
+
+    npx serve .
+
+  Then open http://localhost:5000/test/custom-worker
+-->
+
+<!DOCTYPE html>
+<html>
+<head>
+	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+	<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+	<link rel="stylesheet" data-name="vs/editor/editor.main" href="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.css">
+</head>
+<body>
+
+<h2>Monaco Editor TypeScript test page</h2>
+<button id="resetBtn">Reset Sample</button>
+<div id="container" style="width:800px;height:600px;border:1px solid grey"></div>
+<h3>Custom webworker</h3>
+<button id="logDTS">Log DTS</button>
+<button id="getAST">Print AST to console</button>
+
+<script>
+	var paths = {
+		'vs/basic-languages': '../node_modules/monaco-languages/release/dev',
+		'vs/language/typescript': '../release/dev',
+		'vs': '../node_modules/monaco-editor-core/dev/vs'
+	};
+	if (document.location.protocol === 'http:') {
+		// Add support for running local http server
+		let testIndex = document.location.pathname.indexOf('/test/');
+		if (testIndex !== -1) {
+			let prefix = document.location.pathname.substr(0, testIndex);
+			paths['vs/language/typescript'] = prefix + '/release/dev';
+		}
+	}
+	var require = {
+		paths: paths
+	};
+</script>
+<script src="../node_modules/monaco-editor-core/dev/vs/loader.js"></script>
+<script src="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.nls.js"></script>
+<script src="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.js"></script>
+
+<script>
+	function getDefaultCode() {
+		return [
+			'/* Game of Life',
+			' * Implemented in TypeScript',
+			' * To learn more about TypeScript, please visit http://www.typescriptlang.org/',
+			' */',
+			'',
+			'module Conway {',
+			'',
+			'	export class Cell {',
+			'		public row: number;',
+			'		public col: number;',
+			'		public live: boolean;',
+			'',
+			'		constructor(row: number, col: number, live: boolean) {',
+			'			this.row = row;',
+			'			this.col = col;',
+			'			this.live = live',
+			'		}',
+			'	}',
+			'',
+			'	export class GameOfLife {',
+			'		private gridSize: number;',
+			'		private canvasSize: number;',
+			'		private lineColor: string;',
+			'		private liveColor: string;',
+			'		private deadColor: string;',
+			'		private initialLifeProbability: number;',
+			'		private animationRate: number;',
+			'		private cellSize: number;',
+			'		private context: CanvasRenderingContext2D;',
+			'		private world;',
+			'',
+			'',
+			'		constructor() {',
+			'			this.gridSize = 50;',
+			'			this.canvasSize = 600;',
+			'			this.lineColor = \'#cdcdcd\';',
+			'			this.liveColor = \'#666\';',
+			'			this.deadColor = \'#eee\';',
+			'			this.initialLifeProbability = 0.5;',
+			'			this.animationRate = 60;',
+			'			this.cellSize = 0;',
+			'			this.world = this.createWorld();',
+			'			this.circleOfLife();',
+			'		}',
+			'',
+			'		public createWorld() {',
+			'			return this.travelWorld( (cell : Cell) =>  {',
+			'				cell.live = Math.random() < this.initialLifeProbability;',
+			'				return cell;',
+			'			});',
+			'		}',
+			'',
+			'		public circleOfLife() : void {',
+			'			this.world = this.travelWorld( (cell: Cell) => {',
+			'				cell = this.world[cell.row][cell.col];',
+			'				this.draw(cell);',
+			'				return this.resolveNextGeneration(cell);',
+			'			});',
+			'			setTimeout( () => {this.circleOfLife()}, this.animationRate);',
+			'		}',
+			'',
+			'		public resolveNextGeneration(cell : Cell) {',
+			'			var count = this.countNeighbors(cell);',
+			'			var newCell = new Cell(cell.row, cell.col, cell.live);',
+			'			if(count < 2 || count > 3) newCell.live = false;',
+			'			else if(count == 3) newCell.live = true;',
+			'			return newCell;',
+			'		}',
+			'',
+			'		public countNeighbors(cell : Cell) {',
+			'			var neighbors = 0;',
+			'			for(var row = -1; row <=1; row++) {',
+			'				for(var col = -1; col <= 1; col++) {',
+			'					if(row == 0 && col == 0) continue;',
+			'					if(this.isAlive(cell.row + row, cell.col + col)) {',
+			'						neighbors++;',
+			'					}',
+			'				}',
+			'			}',
+			'			return neighbors;',
+			'		}',
+			'',
+			'		public isAlive(row : number, col : number) {',
+			'			if(row < 0 || col < 0 || row >= this.gridSize || col >= this.gridSize) return false;',
+			'			return this.world[row][col].live;',
+			'		}',
+			'',
+			'		public travelWorld(callback) {',
+			'			var result = [];',
+			'			for(var row = 0; row < this.gridSize; row++) {',
+			'				var rowData = [];',
+			'				for(var col = 0; col < this.gridSize; col++) {',
+			'					rowData.push(callback(new Cell(row, col, false)));',
+			'				}',
+			'				result.push(rowData);',
+			'			}',
+			'			return result;',
+			'		}',
+			'',
+			'		public draw(cell : Cell) {',
+			'			if(this.context == null) this.context = this.createDrawingContext();',
+			'			if(this.cellSize == 0) this.cellSize = this.canvasSize/this.gridSize;',
+			'',
+			'			this.context.strokeStyle = this.lineColor;',
+			'			this.context.strokeRect(cell.row * this.cellSize, cell.col*this.cellSize, this.cellSize, this.cellSize);',
+			'			this.context.fillStyle = cell.live ? this.liveColor : this.deadColor;',
+			'			this.context.fillRect(cell.row * this.cellSize, cell.col*this.cellSize, this.cellSize, this.cellSize);',
+			'		}',
+			'',
+			'		public createDrawingContext() {',
+			'			var canvas = <HTMLCanvasElement> document.getElementById(\'conway-canvas\');',
+			'			if(canvas == null) {',
+			'					canvas = document.createElement(\'canvas\');',
+			'					canvas.id = \'conway-canvas\';',
+			'					canvas.width = this.canvasSize;',
+			'					canvas.height = this.canvasSize;',
+			'					document.body.appendChild(canvas);',
+			'			}',
+			'			return canvas.getContext(\'2d\');',
+			'		}',
+			'	}',
+			'}',
+			'',
+			'var game = new Conway.GameOfLife();',
+		].join('\n');
+	}
+
+	function getDefaultComplierOpts() {
+		return { target: 99, jsx: 1, allowNonTsExtensions: true }
+	}
+	require([
+		'vs/basic-languages/monaco.contribution',
+		'vs/language/typescript/monaco.contribution'
+	], () => {
+
+	monaco.languages.typescript.typescriptDefaults.setWorkerOptions({ customWorkerPath: "http://localhost:5000/test/custom-worker.js" })
+	monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: 99, jsx: 1, allowNonTsExtensions: true, declaration: true, noLibCheck: true })
+
+	var editor = monaco.editor.create(document.getElementById('container'), {
+		value: localStorage.getItem("code") || getDefaultCode(),
+		language: 'typescript',
+		lightbulb: { enabled: true }
+	});
+
+	editor.onDidChangeModelContent(() => {
+		const code = editor.getModel().getValue()
+		localStorage.setItem("code", code)
+	});
+
+	document.getElementById('resetBtn').onclick = () => {
+		editor.setValue(getDefaultCode());
+	};
+
+	document.getElementById('logDTS').onclick = async () => {
+		const model = editor.getModel()
+		const worker = await monaco.languages.typescript.getTypeScriptWorker()
+		const thisWorker = await worker(model.uri)
+		const dts = await thisWorker.getDTSEmitForFile(model.uri.toString())
+		console.log(dts)
+	};
+
+	document.getElementById('getAST').onclick = async () => {
+		const model = editor.getModel()
+		const worker = await monaco.languages.typescript.getTypeScriptWorker()
+		const thisWorker = await worker(model.uri)
+		const ast = await thisWorker.printAST(model.uri.toString())
+		console.log(ast)
+	};
+
+});
+</script>
+</body>
+</html>

+ 60 - 0
test/custom-worker.js

@@ -0,0 +1,60 @@
+// This example uses @typescript/vfs to create a virtual TS program
+// which can do work on a bg thread.
+
+// This version of the vfs edits the global scope (in the case of a webworker, this is 'self')
+importScripts("https://unpkg.com/@typescript/vfs@1.3.0/dist/vfs.globals.js")
+
+/** @type { import("@typescript/vfs") } */
+const tsvfs = globalThis.tsvfs
+
+/** @type {import("../src/tsWorker").CustomTSWebWorkerFactory }*/
+const worker = (TypeScriptWorker, ts, libFileMap) => {
+  return class MonacoTSWorker extends TypeScriptWorker {
+
+    // Adds a custom function to the webworker
+    async getDTSEmitForFile(fileName) {
+      const result = await this.getEmitOutput(fileName)
+      const firstDTS = result.outputFiles.find(o => o.name.endsWith(".d.ts"))
+      return (firstDTS && firstDTS.text) || ""
+    }
+
+    async printAST(fileName) {
+      console.log("Creating virtual TS project")
+      const compilerOptions = this.getCompilationSettings()
+      const fsMap = new Map()
+      for (const key of Object.keys(libFileMap)) {
+        fsMap.set(key, "/" + libFileMap[key])
+      }
+
+      const thisCode =  await this.getScriptText(fileName)
+      fsMap.set("index.ts", thisCode)
+
+      console.log("Starting up TS program")
+      const system = tsvfs.createSystem(fsMap)
+      const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, ts)
+
+      const program = ts.createProgram({
+        rootNames: [...fsMap.keys()],
+        options: compilerOptions,
+        host: host.compilerHost,
+      })
+
+      // Now I can look at the AST for the .ts file too
+      const mainSrcFile = program.getSourceFile("index.ts")
+      let miniAST = "SourceFile"
+
+      const recurse = (parent, depth) => {
+        if (depth > 5) return
+        ts.forEachChild(parent, node => {
+          const spaces = "  ".repeat(depth + 1)
+          miniAST += `\n${spaces}${ts.SyntaxKind[node.kind]}`
+          recurse(node, depth + 1)
+        })
+      }
+      recurse(mainSrcFile, 0)
+      return miniAST
+    }
+  }
+}
+
+self.customTSWorkerFactory = worker