Skip to content

Commit da8727e

Browse files
authored
refactor(oxc): migrate codemirror to modern-monaco (#421)
1 parent 5851c3f commit da8727e

7 files changed

Lines changed: 152 additions & 260 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { TextmateTheme } from 'modern-monaco'
2+
import type * as Monaco from 'modern-monaco/editor-core'
3+
import { init } from 'modern-monaco'
4+
import VitesseDark from 'shiki/themes/vitesse-dark.mjs'
5+
import VitesseLight from 'shiki/themes/vitesse-light.mjs'
6+
7+
// Reuse the exact Vitesse themes Shiki already ships (also used by `createShiki`)
8+
// so the editor keeps the look the previous `codemirror-theme-vitesse` gave it,
9+
// without depending on a CDN theme fetch at runtime.
10+
const lightTheme = VitesseLight as unknown as TextmateTheme
11+
const darkTheme = VitesseDark as unknown as TextmateTheme
12+
13+
const lightThemeId = lightTheme.name
14+
const darkThemeId = darkTheme.name
15+
16+
function getThemeId(dark: boolean) {
17+
return dark ? darkThemeId : lightThemeId
18+
}
19+
20+
let monacoPromise: Promise<typeof Monaco> | null = null
21+
22+
export async function getMonaco(dark: boolean) {
23+
monacoPromise ??= init({
24+
defaultTheme: dark ? darkTheme : lightTheme,
25+
themes: [lightTheme, darkTheme],
26+
})
27+
return monacoPromise
28+
}
29+
30+
export function applyMonacoTheme(monaco: typeof Monaco, dark: boolean) {
31+
monaco.editor.setTheme(getThemeId(dark))
32+
}
33+
34+
const readonlyEditorOptions: Monaco.editor.IStandaloneEditorConstructionOptions = {
35+
automaticLayout: true,
36+
fontFamily: "'Input Mono', 'FiraCode', monospace",
37+
fontSize: 13,
38+
lineNumbers: 'on',
39+
minimap: { enabled: false },
40+
readOnly: true,
41+
renderLineHighlight: 'none',
42+
scrollBeyondLastLine: false,
43+
scrollbar: {
44+
alwaysConsumeMouseWheel: false,
45+
horizontal: 'auto',
46+
horizontalScrollbarSize: 6,
47+
useShadows: false,
48+
vertical: 'auto',
49+
verticalScrollbarSize: 6,
50+
},
51+
}
52+
53+
export function createReadOnlyMonacoEditor(
54+
monaco: typeof Monaco,
55+
container: HTMLElement,
56+
options: Monaco.editor.IStandaloneEditorConstructionOptions = {},
57+
) {
58+
return monaco.editor.create(container, {
59+
...readonlyEditorOptions,
60+
...options,
61+
scrollbar: {
62+
...readonlyEditorOptions.scrollbar,
63+
...options.scrollbar,
64+
},
65+
})
66+
}

packages/oxc/client/app/pages/fmt/config.vue

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script setup lang="ts">
2-
import { EditorView, basicSetup } from 'codemirror'
3-
import { Compartment, EditorState } from '@codemirror/state'
4-
import { json } from '@codemirror/lang-json'
2+
import type * as Monaco from 'modern-monaco/editor-core'
53
import { parse, iterator } from '@humanwhocodes/momoa'
64
import type { MemberNode, ObjectNode, StringNode } from '@humanwhocodes/momoa'
7-
import { vitesseLight, vitesseDark } from 'codemirror-theme-vitesse'
85
import { useAsyncState } from '@vueuse/core'
6+
import { applyMonacoTheme, createReadOnlyMonacoEditor, getMonaco } from '~/composables/monaco'
97
108
const rpc = useRpc()
119
@@ -57,8 +55,10 @@ const isDark = computed(
5755
const editorRef = ref<HTMLDivElement | null>(null)
5856
const currentDocUrl = ref(DEFAULT_DOC_URL)
5957
const iframeLoading = ref(true)
60-
const themeCompartment = new Compartment()
61-
let view: InstanceType<typeof EditorView> | null = null
58+
let monaco: typeof Monaco | null = null
59+
let editor: Monaco.editor.IStandaloneCodeEditor | null = null
60+
let model: Monaco.editor.ITextModel | null = null
61+
let cursorDisposable: Monaco.IDisposable | null = null
6262
6363
interface SectionRange {
6464
from: number
@@ -129,9 +129,12 @@ function getDocUrlAtCursor(pos: number, _content: string): string {
129129
return DEFAULT_DOC_URL
130130
}
131131
132-
function updateDocUrlFromCursor(editorView: InstanceType<typeof EditorView>) {
133-
const content = editorView.state.doc.toString()
134-
const pos = editorView.state.selection.main.head
132+
function updateDocUrlFromCursor() {
133+
if (!editor || !model) return
134+
const position = editor.getPosition()
135+
if (!position) return
136+
const content = model.getValue()
137+
const pos = model.getOffsetAt(position)
135138
const url = getDocUrlAtCursor(pos, content)
136139
if (currentDocUrl.value !== url) {
137140
currentDocUrl.value = url
@@ -141,47 +144,43 @@ function updateDocUrlFromCursor(editorView: InstanceType<typeof EditorView>) {
141144
142145
const initialized = ref(false)
143146
144-
function initEditor() {
147+
async function initEditor() {
145148
if (!editorRef.value || initialized.value || !isReady.value) return
146149
147150
const content = configData.value ?? '{}'
148151
initConfigRanges(content)
149152
150-
view = new EditorView({
151-
parent: editorRef.value,
152-
doc: content,
153-
extensions: [
154-
basicSetup,
155-
json(),
156-
EditorState.readOnly.of(true),
157-
EditorView.editable.of(false),
158-
themeCompartment.of(isDark.value ? vitesseDark : vitesseLight),
159-
EditorView.updateListener.of(update => {
160-
if (update.selectionSet) {
161-
updateDocUrlFromCursor(update.view)
162-
}
163-
}),
164-
EditorView.domEventHandlers({
165-
click: (_event, editorView) => {
166-
updateDocUrlFromCursor(editorView)
167-
},
168-
}),
169-
],
170-
})
171-
172-
updateDocUrlFromCursor(view)
153+
// Claim the slot synchronously so the reactive watcher can't kick off a
154+
// second init while `getMonaco` is awaited.
173155
initialized.value = true
156+
157+
monaco = await getMonaco(isDark.value)
158+
159+
if (!editorRef.value) return
160+
161+
model = monaco.editor.createModel(content, 'json')
162+
editor = createReadOnlyMonacoEditor(monaco, editorRef.value)
163+
editor.setModel(model)
164+
165+
applyMonacoTheme(monaco, isDark.value)
166+
167+
cursorDisposable = editor.onDidChangeCursorPosition(() => updateDocUrlFromCursor())
168+
169+
updateDocUrlFromCursor()
174170
}
175171
176172
watch([editorRef, isReady], () => initEditor(), { immediate: true })
177173
178174
watch(isDark, dark => {
179-
view?.dispatch({ effects: themeCompartment.reconfigure(dark ? vitesseDark : vitesseLight) })
175+
if (monaco) applyMonacoTheme(monaco, dark)
180176
})
181177
182178
onBeforeUnmount(() => {
183-
view?.destroy()
184-
view = null
179+
cursorDisposable?.dispose()
180+
editor?.dispose()
181+
model?.dispose()
182+
editor = null
183+
model = null
185184
})
186185
</script>
187186

@@ -194,7 +193,7 @@ onBeforeUnmount(() => {
194193
<div
195194
class="flex-1 min-h-0 min-w-0 border-b lg:border-b-0 lg:border-r border-neutral-200 dark:border-neutral-700"
196195
>
197-
<div ref="editorRef" class="h-full min-h-[200px] lg:min-h-0 overflow-auto" />
196+
<div ref="editorRef" class="h-full min-h-[200px] lg:min-h-0" />
198197
</div>
199198
<div class="flex-1 min-h-[200px] lg:min-h-0 min-w-0 relative">
200199
<iframe
@@ -228,7 +227,9 @@ onBeforeUnmount(() => {
228227
</template>
229228

230229
<style>
231-
.cm-editor {
232-
height: 100%;
230+
.monaco-editor,
231+
.monaco-editor .overflow-guard {
232+
width: 100% !important;
233+
height: 100% !important;
233234
}
234235
</style>

packages/oxc/client/app/pages/lint/config.vue

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script setup lang="ts">
2-
import { EditorView, basicSetup } from 'codemirror'
3-
import { Compartment, EditorState } from '@codemirror/state'
4-
import { json } from '@codemirror/lang-json'
2+
import type * as Monaco from 'modern-monaco/editor-core'
53
import { parse, iterator } from '@humanwhocodes/momoa'
64
import type { MemberNode, ObjectNode, StringNode } from '@humanwhocodes/momoa'
7-
import { vitesseLight, vitesseDark } from 'codemirror-theme-vitesse'
85
import { useAsyncState } from '@vueuse/core'
6+
import { applyMonacoTheme, createReadOnlyMonacoEditor, getMonaco } from '~/composables/monaco'
97
108
const rpc = useRpc()
119
@@ -42,8 +40,10 @@ const isDark = computed(
4240
const editorRef = ref<HTMLDivElement | null>(null)
4341
const currentDocUrl = ref(DEFAULT_DOC_URL)
4442
const iframeLoading = ref(true)
45-
const themeCompartment = new Compartment()
46-
let view: InstanceType<typeof EditorView> | null = null
43+
let monaco: typeof Monaco | null = null
44+
let editor: Monaco.editor.IStandaloneCodeEditor | null = null
45+
let model: Monaco.editor.ITextModel | null = null
46+
let cursorDisposable: Monaco.IDisposable | null = null
4747
4848
interface RuleRange {
4949
key: string
@@ -190,9 +190,12 @@ function getDocUrlAtCursor(pos: number, content: string): string {
190190
return DEFAULT_DOC_URL
191191
}
192192
193-
function updateDocUrlFromCursor(editorView: InstanceType<typeof EditorView>) {
194-
const content = editorView.state.doc.toString()
195-
const pos = editorView.state.selection.main.head
193+
function updateDocUrlFromCursor() {
194+
if (!editor || !model) return
195+
const position = editor.getPosition()
196+
if (!position) return
197+
const content = model.getValue()
198+
const pos = model.getOffsetAt(position)
196199
const url = getDocUrlAtCursor(pos, content)
197200
if (currentDocUrl.value !== url) {
198201
currentDocUrl.value = url
@@ -202,47 +205,43 @@ function updateDocUrlFromCursor(editorView: InstanceType<typeof EditorView>) {
202205
203206
const initialized = ref(false)
204207
205-
function initEditor() {
208+
async function initEditor() {
206209
if (!editorRef.value || initialized.value || !isReady.value) return
207210
208211
const content = configData.value ?? '{}'
209212
initConfigRanges(content)
210213
211-
view = new EditorView({
212-
parent: editorRef.value,
213-
doc: content,
214-
extensions: [
215-
basicSetup,
216-
json(),
217-
EditorState.readOnly.of(true),
218-
EditorView.editable.of(false),
219-
themeCompartment.of(isDark.value ? vitesseDark : vitesseLight),
220-
EditorView.updateListener.of(update => {
221-
if (update.selectionSet) {
222-
updateDocUrlFromCursor(update.view)
223-
}
224-
}),
225-
EditorView.domEventHandlers({
226-
click: (_event, editorView) => {
227-
updateDocUrlFromCursor(editorView)
228-
},
229-
}),
230-
],
231-
})
232-
233-
updateDocUrlFromCursor(view)
214+
// Claim the slot synchronously so the reactive watcher can't kick off a
215+
// second init while `getMonaco` is awaited.
234216
initialized.value = true
217+
218+
monaco = await getMonaco(isDark.value)
219+
220+
if (!editorRef.value) return
221+
222+
model = monaco.editor.createModel(content, 'json')
223+
editor = createReadOnlyMonacoEditor(monaco, editorRef.value)
224+
editor.setModel(model)
225+
226+
applyMonacoTheme(monaco, isDark.value)
227+
228+
cursorDisposable = editor.onDidChangeCursorPosition(() => updateDocUrlFromCursor())
229+
230+
updateDocUrlFromCursor()
235231
}
236232
237233
watch([editorRef, isReady], () => initEditor(), { immediate: true })
238234
239235
watch(isDark, dark => {
240-
view?.dispatch({ effects: themeCompartment.reconfigure(dark ? vitesseDark : vitesseLight) })
236+
if (monaco) applyMonacoTheme(monaco, dark)
241237
})
242238
243239
onBeforeUnmount(() => {
244-
view?.destroy()
245-
view = null
240+
cursorDisposable?.dispose()
241+
editor?.dispose()
242+
model?.dispose()
243+
editor = null
244+
model = null
246245
})
247246
</script>
248247

@@ -255,7 +254,7 @@ onBeforeUnmount(() => {
255254
<div
256255
class="flex-1 min-h-0 min-w-0 border-b lg:border-b-0 lg:border-r border-neutral-200 dark:border-neutral-700"
257256
>
258-
<div ref="editorRef" class="h-full min-h-[200px] lg:min-h-0 overflow-auto" />
257+
<div ref="editorRef" class="h-full min-h-[200px] lg:min-h-0" />
259258
</div>
260259
<div class="flex-1 min-h-[200px] lg:min-h-0 min-w-0 relative">
261260
<iframe
@@ -289,7 +288,9 @@ onBeforeUnmount(() => {
289288
</template>
290289

291290
<style>
292-
.cm-editor {
293-
height: 100%;
291+
.monaco-editor,
292+
.monaco-editor .overflow-guard {
293+
width: 100% !important;
294+
height: 100% !important;
294295
}
295296
</style>

packages/oxc/client/nuxt.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,8 @@ export default defineNuxtConfig({
7373
},
7474
vite: {
7575
base: BASE,
76+
optimizeDeps: {
77+
include: ['modern-monaco'],
78+
},
7679
},
7780
})

packages/oxc/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"tinyexec": "catalog:deps"
5555
},
5656
"devDependencies": {
57-
"@codemirror/lang-json": "catalog:frontend",
5857
"@humanwhocodes/momoa": "catalog:frontend",
5958
"@nuxt/kit": "catalog:build",
6059
"@nuxt/ui": "catalog:frontend",
@@ -64,9 +63,8 @@
6463
"@vitejs/devtools-ui": "workspace:*",
6564
"@vueuse/core": "catalog:frontend",
6665
"@vueuse/nuxt": "catalog:build",
67-
"codemirror": "catalog:frontend",
68-
"codemirror-theme-vitesse": "catalog:frontend",
6966
"devframe": "catalog:deps",
67+
"modern-monaco": "catalog:frontend",
7068
"nuxt": "catalog:build",
7169
"oxfmt": "catalog:devtools",
7270
"oxlint": "catalog:devtools",

0 commit comments

Comments
 (0)