| 1 | import type { MonacoSetupReturn } from '@slidev/types' |
| 2 | import { createSingletonPromise } from '@antfu/utils' |
| 3 | import { shikiToMonaco } from '@shikijs/monaco' |
| 4 | import { setupTypeAcquisition } from '@typescript/ata' |
| 5 | import * as monaco from 'monaco-editor' |
| 6 | import EditorWorker from 'monaco-editor/editor/editor.worker?worker' |
| 7 | // @ts-expect-error missing types |
| 8 | import { StandaloneServices } from 'monaco-editor/editor/standalone/browser/standaloneServices' |
| 9 | |
| 10 | import CssWorker from 'monaco-editor/language/css/css.worker?worker' |
| 11 | import HtmlWorker from 'monaco-editor/language/html/html.worker?worker' |
| 12 | import JsonWorker from 'monaco-editor/language/json/json.worker?worker' |
| 13 | import TsWorker from 'monaco-editor/language/typescript/ts.worker?worker' |
| 14 | // @ts-expect-error missing types |
| 15 | import { ContextViewService } from 'monaco-editor/platform/contextview/browser/contextViewService' |
| 16 | // @ts-expect-error missing types |
| 17 | import { SyncDescriptor } from 'monaco-editor/platform/instantiation/common/descriptors' |
| 18 | import ts from 'typescript' |
| 19 | import { watchEffect } from 'vue' |
| 20 | |
| 21 | import configs from '#slidev/configs' |
| 22 | import setups from '#slidev/setups/monaco' |
| 23 | import { isDark } from '../logic/dark' |
| 24 | import { lockShortcuts } from '../state' |
| 25 | |
| 26 | window.MonacoEnvironment = { |
| 27 | getWorker(_, label) { |
| 28 | if (label === 'json') |
| 29 | return new JsonWorker() |
| 30 | if (label === 'css' || label === 'scss' || label === 'less') |
| 31 | return new CssWorker() |
| 32 | if (label === 'html' || label === 'handlebars' || label === 'razor') |
| 33 | return new HtmlWorker() |
| 34 | if (label === 'typescript' || label === 'javascript') |
| 35 | return new TsWorker() |
| 36 | return new EditorWorker() |
| 37 | }, |
| 38 | } |
| 39 | |
| 40 | class ContextViewService2 extends ContextViewService { |
| 41 | showContextView(...args: any) { |
| 42 | super.showContextView(...args) |
| 43 | // @ts-expect-error missing types |
| 44 | const contextView = this.contextView.view as HTMLElement |
| 45 | contextView.style.left = `calc(${contextView.style.left} / var(--slidev-slide-scale))` |
| 46 | contextView.style.top = `calc(${contextView.style.top} / var(--slidev-slide-scale))` |
| 47 | // Reset the scale to 1. Otherwise, the sub-menu will be in the wrong position. |
| 48 | contextView.style.transform = `scale(calc(1 / var(--slidev-slide-scale)))` |
| 49 | contextView.style.transformOrigin = '0 0' |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // Initialize services first, otherwise we can't override them. |
| 54 | StandaloneServices.initialize({ |
| 55 | contextViewService: new SyncDescriptor(ContextViewService2, [], true), |
| 56 | }) |
| 57 | |
| 58 | const setup = createSingletonPromise(async () => { |
| 59 | const defaults = monaco.typescript.typescriptDefaults |
| 60 | defaults.setCompilerOptions({ |
| 61 | ...defaults.getCompilerOptions(), |
| 62 | strict: true, |
| 63 | moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs, |
| 64 | module: monaco.typescript.ModuleKind.ESNext, |
| 65 | }) |
| 66 | |
| 67 | const ata = configs.monacoTypesSource === 'cdn' |
| 68 | ? setupTypeAcquisition({ |
| 69 | projectName: 'TypeScript Playground', |
| 70 | typescript: ts as any, // Version mismatch. No problem found so far. |
| 71 | logger: console, |
| 72 | delegate: { |
| 73 | receivedFile: (code: string, path: string) => { |
| 74 | defaults.addExtraLib(code, `file://${path}`) |
| 75 | const uri = monaco.Uri.file(path) |
| 76 | if (monaco.editor.getModel(uri) === null) |
| 77 | monaco.editor.createModel(code, 'javascript', uri) |
| 78 | }, |
| 79 | progress: (downloaded: number, total: number) => { |
| 80 | // eslint-disable-next-line no-console |
| 81 | console.debug(`[Typescript ATA] ${downloaded} / ${total}`) |
| 82 | }, |
| 83 | }, |
| 84 | }) |
| 85 | : () => { } |
| 86 | |
| 87 | const { getEagerHighlighter, languageNames, themeOption } = await (await import('./shiki')).default() |
| 88 | |
| 89 | monaco.languages.register({ id: 'vue' }) |
| 90 | monaco.languages.register({ id: 'html' }) |
| 91 | monaco.languages.register({ id: 'css' }) |
| 92 | monaco.languages.register({ id: 'typescript' }) |
| 93 | monaco.languages.register({ id: 'javascript' }) |
| 94 | for (const lang of languageNames) { |
| 95 | monaco.languages.register({ id: lang }) |
| 96 | } |
| 97 | |
| 98 | const editorOptions: MonacoSetupReturn['editorOptions'] & object = {} |
| 99 | for (const setup of setups) { |
| 100 | const result = await setup(monaco) |
| 101 | Object.assign(editorOptions, result?.editorOptions) |
| 102 | } |
| 103 | |
| 104 | // Disable shortcuts when focusing Monaco editor. |
| 105 | monaco.editor.onDidCreateEditor((editor) => { |
| 106 | let release: (() => void) | null = null |
| 107 | editor.onDidFocusEditorWidget(() => { |
| 108 | release = lockShortcuts() |
| 109 | }) |
| 110 | editor.onDidBlurEditorWidget(() => { |
| 111 | release?.() |
| 112 | release = null |
| 113 | }) |
| 114 | }) |
| 115 | |
| 116 | // Use Shiki to highlight Monaco |
| 117 | const highlighter = await getEagerHighlighter() |
| 118 | shikiToMonaco(highlighter, monaco) |
| 119 | if (typeof themeOption === 'string') { |
| 120 | monaco.editor.setTheme(themeOption) |
| 121 | } |
| 122 | else { |
| 123 | watchEffect(() => { |
| 124 | monaco.editor.setTheme(isDark.value |
| 125 | ? themeOption.dark || 'vitesse-dark' |
| 126 | : themeOption.light || 'vitesse-light') |
| 127 | }) |
| 128 | } |
| 129 | |
| 130 | return { |
| 131 | monaco, |
| 132 | ata, |
| 133 | editorOptions, |
| 134 | } |
| 135 | }) |
| 136 | |
| 137 | async function _addFile(raw: () => Promise<{ default: string }>, path: string) { |
| 138 | const uri = monaco.Uri.file(path) |
| 139 | const code = (await raw()).default |
| 140 | monaco.typescript.typescriptDefaults.addExtraLib(code, `file:///${path}`) |
| 141 | monaco.editor.createModel(code, 'javascript', uri) |
| 142 | } |
| 143 | |
| 144 | const addFileCache = new Map<string, Promise<void>>() |
| 145 | |
| 146 | export async function addFile(raw: () => Promise<{ default: string }>, path: string) { |
| 147 | if (addFileCache.has(path)) |
| 148 | return addFileCache.get(path) |
| 149 | const promise = _addFile(raw, path) |
| 150 | addFileCache.set(path, promise) |
| 151 | return promise |
| 152 | } |
| 153 | |
| 154 | export default setup |
| 155 |