Files
kandev/apps/web/components/editors/monaco/monaco-loader.ts
T
2026-02-19 22:18:01 +00:00

167 lines
6.3 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 };