addons/isl/src/ComparisonView/SplitDiffView/syntaxHighlightingWorker.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {IGrammar} from 'vscode-textmate';
b69ab319import type {ThemeColor} from '../../theme';
b69ab3110import type {
b69ab3111 SyntaxWorkerRequest,
b69ab3112 SyntaxWorkerResponse,
b69ab3113 TokenizedDiffHunks,
b69ab3114 TokenizedHunk,
b69ab3115} from './syntaxHighlightingTypes';
b69ab3116
b69ab3117import {CancellationToken} from 'shared/CancellationToken';
b69ab3118import FilepathClassifier from 'shared/textmate-lib/FilepathClassifier';
b69ab3119import {tokenizeLines} from 'shared/textmate-lib/tokenize';
b69ab3120import {loadWASM} from 'vscode-oniguruma';
b69ab3121import {grammars, languages} from '../../generated/textmate/TextMateGrammarManifest';
b69ab3122import {getGrammar, getGrammarStore} from './grammar';
b69ab3123
b69ab3124const URL_TO_ONIG_WASM = './generated/textmate/onig.wasm';
b69ab3125
b69ab3126/* This file is intended to be executed in a WebWorker, without access to the DOM. */
b69ab3127
b69ab3128/**
b69ab3129 * Fetch requests inside the webworker must be made relative to the base URI.
b69ab3130 * By executing the web worker via a blob: URL, we can't depend on the base being inherited.
b69ab3131 */
b69ab3132let globalBaseUri: string;
b69ab3133
b69ab3134async function loadGrammar(
b69ab3135 theme: ThemeColor,
b69ab3136 path: string,
b69ab3137 postMessage: (msg: SyntaxWorkerResponse) => void,
b69ab3138): Promise<IGrammar | undefined> {
b69ab3139 await ensureOnigurumaIsLoaded(globalBaseUri);
b69ab3140
b69ab3141 const scopeName = getFilepathClassifier().findScopeNameForPath(path);
b69ab3142 if (!scopeName) {
b69ab3143 return undefined;
b69ab3144 }
b69ab3145
b69ab3146 const store = getGrammarStore(theme, globalBaseUri, colorMap => {
b69ab3147 // tell client the newest colorMap
b69ab3148 postMessage({type: 'cssColorMap', colorMap} as SyntaxWorkerResponse);
b69ab3149 });
b69ab3150
b69ab3151 const grammar = await getGrammar(store, scopeName);
b69ab3152 return grammar ?? undefined;
b69ab3153}
b69ab3154
b69ab3155const cancellationTokenForId = new Map<number, CancellationToken>();
b69ab3156
b69ab3157class WorkQueue {
b69ab3158 private queue: Array<() => Promise<void>> = [];
b69ab3159 private isProcessing = false;
b69ab3160
b69ab3161 public push(work: () => Promise<void>) {
b69ab3162 this.queue.push(work);
b69ab3163
b69ab3164 if (!this.isProcessing) {
b69ab3165 this.processNext();
b69ab3166 }
b69ab3167 }
b69ab3168
b69ab3169 private async processNext() {
b69ab3170 if (this.queue.length > 0) {
b69ab3171 const work = this.queue.shift();
b69ab3172 this.isProcessing = true;
b69ab3173 // Allow the task queue to be emptied before continuing,
b69ab3174 // so we can process cancel messages
b69ab3175 await new Promise(res => setTimeout(res, 0));
b69ab3176 await work?.().catch(err => {
b69ab3177 // eslint-disable-next-line no-console
b69ab3178 console.error(err);
b69ab3179 return null;
b69ab3180 });
b69ab3181 this.isProcessing = false;
b69ab3182 this.processNext();
b69ab3183 }
b69ab3184 }
b69ab3185}
b69ab3186
b69ab3187const workQueue = new WorkQueue();
b69ab3188
b69ab3189export function handleMessage(
b69ab3190 postMessage: (msg: SyntaxWorkerResponse & {id?: number}) => unknown,
b69ab3191 event: MessageEvent,
b69ab3192) {
b69ab3193 const data = event.data as SyntaxWorkerRequest & {id: number};
b69ab3194
b69ab3195 const token = new CancellationToken();
b69ab3196 if (data.id != null) {
b69ab3197 cancellationTokenForId.set(data.id, token);
b69ab3198 }
b69ab3199 switch (data.type) {
b69ab31100 case 'setBaseUri': {
b69ab31101 globalBaseUri = data.base;
b69ab31102 break;
b69ab31103 }
b69ab31104 case 'tokenizeContents': {
b69ab31105 workQueue.push(async () => {
b69ab31106 const grammar = await loadGrammar(data.theme, data.path, postMessage);
b69ab31107 const result = tokenizeContent(grammar, data.content, token);
b69ab31108 postMessage({type: data.type, id: data.id, result});
b69ab31109 cancellationTokenForId.delete(data.id);
b69ab31110 });
b69ab31111 break;
b69ab31112 }
b69ab31113 case 'tokenizeHunks': {
b69ab31114 workQueue.push(async () => {
b69ab31115 const grammar = await loadGrammar(data.theme, data.path, postMessage);
b69ab31116 const result = tokenizeHunks(grammar, data.hunks, token);
b69ab31117 postMessage({type: data.type, id: data.id, result});
b69ab31118 cancellationTokenForId.delete(data.id);
b69ab31119 });
b69ab31120 break;
b69ab31121 }
b69ab31122 case 'cancel': {
b69ab31123 const token = cancellationTokenForId.get(data.idToCancel);
b69ab31124 token?.cancel();
b69ab31125 }
b69ab31126 }
b69ab31127}
b69ab31128
b69ab31129if (typeof self.document === 'undefined') {
b69ab31130 // inside WebWorker, use global onmessage and postMessage
b69ab31131 onmessage = handleMessage.bind(undefined, postMessage);
b69ab31132 // outside of a WebWorker, the exported `handleMessage` function should be used instead.
b69ab31133}
b69ab31134
b69ab31135function tokenizeHunks(
b69ab31136 grammar: IGrammar | undefined,
b69ab31137 hunks: Array<{lines: Array<string>}>,
b69ab31138 cancellationToken: CancellationToken,
b69ab31139): TokenizedDiffHunks | undefined {
b69ab31140 if (grammar == null) {
b69ab31141 return undefined;
b69ab31142 }
b69ab31143
b69ab31144 if (cancellationToken.isCancelled) {
b69ab31145 // check for cancellation before doing expensive highlighting
b69ab31146 return undefined;
b69ab31147 }
b69ab31148
b69ab31149 const tokenizedPatches: TokenizedDiffHunks = hunks
b69ab31150 .map(hunk => recoverFileContentsFromPatchLines(hunk.lines))
b69ab31151 .map(([before, after]) => [tokenizeLines(before, grammar), tokenizeLines(after, grammar)]);
b69ab31152
b69ab31153 return tokenizedPatches;
b69ab31154}
b69ab31155
b69ab31156function tokenizeContent(
b69ab31157 grammar: IGrammar | undefined,
b69ab31158 content: Array<string>,
b69ab31159 cancellationToken: CancellationToken,
b69ab31160): TokenizedHunk | undefined {
b69ab31161 if (grammar == null) {
b69ab31162 return undefined;
b69ab31163 }
b69ab31164
b69ab31165 if (cancellationToken.isCancelled) {
b69ab31166 // check for cancellation before doing expensive highlighting
b69ab31167 return undefined;
b69ab31168 }
b69ab31169
b69ab31170 return tokenizeLines(content, grammar);
b69ab31171}
b69ab31172
b69ab31173/**
b69ab31174 * Patch lines start with ' ', '+', or '-'. From this we can reconstruct before & after file contents as strings,
b69ab31175 * which we can actually use in the syntax highlighting.
b69ab31176 */
b69ab31177function recoverFileContentsFromPatchLines(
b69ab31178 lines: Array<string>,
b69ab31179): [before: Array<string>, after: Array<string>] {
b69ab31180 const linesBefore = [];
b69ab31181 const linesAfter = [];
b69ab31182 for (const line of lines) {
b69ab31183 if (line[0] === ' ') {
b69ab31184 linesBefore.push(line.slice(1));
b69ab31185 linesAfter.push(line.slice(1));
b69ab31186 } else if (line[0] === '+') {
b69ab31187 linesAfter.push(line.slice(1));
b69ab31188 } else if (line[0] === '-') {
b69ab31189 linesBefore.push(line.slice(1));
b69ab31190 }
b69ab31191 }
b69ab31192
b69ab31193 return [linesBefore, linesAfter];
b69ab31194}
b69ab31195
b69ab31196let onigurumaLoadingJob: Promise<void> | null = null;
b69ab31197function ensureOnigurumaIsLoaded(base: string): Promise<void> {
b69ab31198 if (onigurumaLoadingJob === null) {
b69ab31199 onigurumaLoadingJob = loadOniguruma(base);
b69ab31200 }
b69ab31201 return onigurumaLoadingJob;
b69ab31202}
b69ab31203
b69ab31204async function loadOniguruma(base: string): Promise<void> {
b69ab31205 const url = new URL(URL_TO_ONIG_WASM, base);
b69ab31206 const onigurumaWASMRequest = fetch(url);
b69ab31207 const response = await onigurumaWASMRequest;
b69ab31208
b69ab31209 const contentType = response.headers.get('content-type');
b69ab31210 const useStreamingParser = contentType === 'application/wasm';
b69ab31211
b69ab31212 if (useStreamingParser) {
b69ab31213 await loadWASM(response);
b69ab31214 } else {
b69ab31215 const dataOrOptions = {
b69ab31216 data: await response.arrayBuffer(),
b69ab31217 };
b69ab31218 await loadWASM(dataOrOptions);
b69ab31219 }
b69ab31220}
b69ab31221
b69ab31222let _classifier: FilepathClassifier | null = null;
b69ab31223
b69ab31224function getFilepathClassifier(): FilepathClassifier {
b69ab31225 if (_classifier == null) {
b69ab31226 _classifier = new FilepathClassifier(grammars, languages);
b69ab31227 }
b69ab31228 return _classifier;
b69ab31229}