Skip to content

Commit

Permalink
Implement Custom TS Worker (#625)
Browse files Browse the repository at this point in the history
* Add esbuild for correctly bundling workers
* Modify MonacoEditor.svelte to use custom TS worker
* Includes a minimal overridded typescript worker, including a sample completion injection
* Fix typing for custom methods
* Built workers in `npm run build`, ensure worker config does not crash
  • Loading branch information
Cobular authored May 17, 2023
1 parent 075707a commit edd7bc1
Show file tree
Hide file tree
Showing 13 changed files with 1,006 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
ignorePatterns: ['*.cjs'],
ignorePatterns: ['*.cjs', 'static/*.worker.js', 'static/*.worker.js.map'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand Down
73 changes: 71 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,19 @@
"@types/d3-shape": "^3.1.1",
"@types/d3-time": "^3.0.0",
"@types/lodash-es": "^4.17.7",
"@types/picomatch": "^2.3.0",
"@types/toastify-js": "^1.11.1",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@vitest/ui": "^0.30.1",
"cloc": "^2.11.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"jsdom": "^21.1.1",
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"postcss-html": "^1.5.0",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.0",
Expand Down
10 changes: 10 additions & 0 deletions src/components/sequencing/SequenceEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<svelte:options immutable={true} />

<script lang="ts">
import type { editor as Editor, languages } from 'monaco-editor/esm/vs/editor/editor.api';
import { createEventDispatcher } from 'svelte';
import { userSequencesRows } from '../../stores/sequencing';
import type { Monaco, TypeScriptFile } from '../../types/monaco';
Expand Down Expand Up @@ -48,6 +49,14 @@
a.download = sequenceName;
a.click();
}
function fullyLoaded(
event: CustomEvent<{ model: Editor.ITextModel; worker: languages.typescript.TypeScriptWorker }>,
) {
const { model, worker } = event.detail;
const model_id = model.id;
console.log(`Model ${model_id} loaded!`, worker);
}
</script>

<CssGrid bind:rows={$userSequencesRows}>
Expand All @@ -73,6 +82,7 @@
tabSize={2}
value={sequenceDefinition}
on:didChangeModelContent
on:fullyLoaded={fullyLoaded}
/>
</svelte:fragment>
</Panel>
Expand Down
47 changes: 43 additions & 4 deletions src/components/ui/MonacoEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<svelte:options accessors={true} immutable={true} />

<script lang="ts">
import type { editor as Editor } from 'monaco-editor/esm/vs/editor/editor.api';
import type { editor as Editor, IDisposable, Uri, languages } from 'monaco-editor/esm/vs/editor/editor.api';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import type { Monaco } from '../../types/monaco';
import { ShouldRetryError, promiseRetry } from '../../utilities/generic';
export { className as class };
export { styleName as style };
Expand All @@ -24,11 +24,17 @@
export let theme: string | undefined = undefined;
export let value: string | undefined = undefined;
const dispatch = createEventDispatcher();
type TypeScriptWorker = languages.typescript.TypeScriptWorker;
const dispatch = createEventDispatcher<{
didChangeModelContent: { e: Editor.IModelContentChangedEvent; value: string };
fullyLoaded: { model: Editor.ITextModel; worker: TypeScriptWorker };
}>();
let className: string = '';
let div: HTMLDivElement | undefined = undefined;
let editor: Editor.IStandaloneCodeEditor | undefined = undefined;
let editor_load_event: IDisposable | undefined = undefined;
let styleName: string = '';
$: if (editor) {
Expand All @@ -50,7 +56,9 @@
return new jsonWorker();
}
if (label === 'typescript' || label === 'javascript') {
return new tsWorker();
// Force the worker to be loaded in the classic style, served directly out of static
// https://thethoughtfulkoala.com/posts/2021/07/10/vite-js-classic-web-worker.html
return new Worker(new URL('ts.worker.js', location.origin), { type: 'classic' });
}
return new editorWorker();
},
Expand All @@ -70,18 +78,49 @@
value,
};
monaco = await import('monaco-editor');
monaco.languages.typescript.typescriptDefaults.setWorkerOptions({
customWorkerPath: '/customTS.worker.js',
});
editor = monaco.editor.create(div, options, override);
editor.onDidChangeModelContent((e: Editor.IModelContentChangedEvent) => {
const newValue = editor.getModel().getValue();
dispatch('didChangeModelContent', { e, value: newValue });
});
// So.. there is no way to check when the model is initialized apparently!
// https://github.com/microsoft/monaco-editor/issues/115
// If we accidentally call the `getTypeScriptWorker()` function to early, it throws.
// Just use retry with exponential back-off to get it!
promiseRetry(
async () => {
let getWorker: (...uris: Uri[]) => Promise<TypeScriptWorker>;
let tsWorker: TypeScriptWorker;
// Errors in this block indicate failure to find a loaded worker
// so transform to the specific error type we care about
try {
getWorker = await monaco.languages.typescript.getTypeScriptWorker();
tsWorker = await getWorker();
} catch (e) {
throw new ShouldRetryError();
}
// Errors in the dispatch won't trigger the retry and will just fail.
dispatch('fullyLoaded', { model: editor.getModel(), worker: tsWorker });
},
5,
10,
);
});
onDestroy(() => {
if (editor) {
editor.dispose();
}
if (editor_load_event) {
editor_load_event.dispose();
}
});
</script>

Expand Down
18 changes: 18 additions & 0 deletions src/custom-typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Specify overriden types for certain modules.
*
* In this case, we need to add our typescript worker method additions to the `monaco-editor` internal
* worker type. The following has the effect of applying such a patch, utalizing decleration merging
* to add our prop overrides to the type `monaco-editor/languages/typescript/TypeScriptWorker` at all uses.
*/

import type { WorkerOverrideProps } from './workers/customTS.worker';
declare module 'monaco-editor' {
namespace languages.typescript {
// The transformation from interface extension to type decleration that eslint wants to make
// breaks the [decleration merge](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)
// that's happening here.
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface TypeScriptWorker extends WorkerOverrideProps {}
}
}
Loading

0 comments on commit edd7bc1

Please sign in to comment.