Files
kandev/apps/web/components/editors/monaco/monaco-loader.ts
T
2026-02-16 09:55:04 +00:00

117 lines
5.9 KiB
TypeScript

import * as monacoImport from 'monaco-editor';
import type { Monaco } from '@monaco-editor/react';
import type { IDisposable } from 'monaco-editor';
import { loader } from '@monaco-editor/react';
import { isBuiltinTsSuppressed } from './builtin-providers';
// Cast to Monaco type (from @monaco-editor/react) which has the full
// languages.typescript typings. The main 'monaco-editor' export marks
// languages.typescript as deprecated in favor of top-level typescript
// namespace, but @monaco-editor/react's Monaco type still uses it.
const monaco = monacoImport as unknown as Monaco;
// ---------------------------------------------------------------------------
// Built-in TS/JS provider suppression
// ---------------------------------------------------------------------------
// Monaco v0.55.1's setModeConfiguration fires onDidChange, but the
// tsMode.js setupMode() function never subscribes to it — providers are
// registered once and never torn down. So we intercept provider registration
// to wrap built-in TS/JS providers with a suppression flag check. When LSP
// is active, the wrappers return null/empty instead of calling the original.
//
// Key: we check `isBuiltinTsSuppressed()` at REGISTRATION time. If the flag
// is already true when a provider is registered, it means the LSP client is
// registering its own providers — those should NOT be wrapped. Only providers
// registered while the flag is false (Monaco's built-in ones) get wrapped.
// ---------------------------------------------------------------------------
const TS_LANGUAGES = new Set([
'typescript',
'javascript',
'typescriptreact',
'javascriptreact',
]);
// Configure workers before Monaco initializes.
// The `new URL('...', import.meta.url)` pattern is recognized by Turbopack/webpack
// to bundle each worker as a separate chunk.
if (typeof window !== 'undefined') {
self.MonacoEnvironment = {
getWorker(_workerId: string, label: string) {
switch (label) {
case 'json':
return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url), { type: 'module' });
case 'css':
case 'scss':
case 'less':
return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url), { type: 'module' });
case 'html':
case 'handlebars':
case 'razor':
return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url), { type: 'module' });
case 'typescript':
case 'javascript':
return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url), { type: 'module' });
default:
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), { type: 'module' });
}
},
};
// Tell @monaco-editor/react to use local monaco instance (no CDN fetch)
loader.config({ monaco: monacoImport });
// Expose on window for code that accesses window.monaco (e.g. keybindings in editor)
(window as { monaco?: Monaco }).monaco = monaco;
// Wrap built-in TS/JS provider registration so we can suppress them at call
// time when an external LSP is active. This MUST happen before the first TS
// model is created (which triggers the lazy tsMode setup), and since this
// module runs at import time, it's guaranteed to be in place early enough.
const langs = monacoImport.languages;
type ProviderRegistrationFn = (selector: string, provider: Record<string, unknown>, ...rest: unknown[]) => IDisposable;
function wrapRegistration(
original: ProviderRegistrationFn,
methodName: string,
emptyResult: unknown,
): ProviderRegistrationFn {
return function (selector: string, provider: Record<string, unknown>, ...rest: unknown[]) {
// Only wrap providers for TS/JS languages that are registered while
// suppression is OFF (= Monaco's built-in providers). When suppression
// is already ON at registration time, it means the LSP client is
// registering its own providers — pass those through unwrapped.
if (
typeof selector === 'string' &&
TS_LANGUAGES.has(selector) &&
typeof provider[methodName] === 'function' &&
!isBuiltinTsSuppressed()
) {
const origMethod = (provider[methodName] as (...a: unknown[]) => unknown).bind(provider);
const wrapped = Object.create(provider);
wrapped[methodName] = function (...args: unknown[]) {
if (isBuiltinTsSuppressed()) return emptyResult;
return origMethod(...args);
};
return original.call(langs, selector, wrapped, ...rest);
}
return original.call(langs, selector, provider, ...rest);
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- monkey-patching Monaco internals
const l = langs as Record<string, any>;
l.registerHoverProvider = wrapRegistration(l.registerHoverProvider.bind(langs), 'provideHover', null);
l.registerCompletionItemProvider = wrapRegistration(l.registerCompletionItemProvider.bind(langs), 'provideCompletionItems', { suggestions: [] });
l.registerSignatureHelpProvider = wrapRegistration(l.registerSignatureHelpProvider.bind(langs), 'provideSignatureHelp', null);
l.registerDefinitionProvider = wrapRegistration(l.registerDefinitionProvider.bind(langs), 'provideDefinition', null);
l.registerReferenceProvider = wrapRegistration(l.registerReferenceProvider.bind(langs), 'provideReferences', null);
l.registerDocumentHighlightProvider = wrapRegistration(l.registerDocumentHighlightProvider.bind(langs), 'provideDocumentHighlights', null);
l.registerCodeActionProvider = wrapRegistration(l.registerCodeActionProvider.bind(langs), 'provideCodeActions', null);
l.registerRenameProvider = wrapRegistration(l.registerRenameProvider.bind(langs), 'provideRenameEdits', null);
l.registerInlayHintsProvider = wrapRegistration(l.registerInlayHintsProvider.bind(langs), 'provideInlayHints', null);
}
export { monaco };