Skip to content

Commit 6b02f38

Browse files
authored
refactor(rolldown): migrate codemirror to modern-monaco (#185)
1 parent fe353ef commit 6b02f38

File tree

11 files changed

+526
-354
lines changed

11 files changed

+526
-354
lines changed

packages/rolldown/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
"ws": "catalog:deps"
6363
},
6464
"devDependencies": {
65-
"@types/codemirror": "catalog:types",
6665
"@types/diff": "catalog:types",
6766
"@types/split2": "catalog:types",
6867
"@types/splitpanes": "catalog:types",
@@ -72,15 +71,14 @@
7271
"@vueuse/core": "catalog:frontend",
7372
"@vueuse/nuxt": "catalog:build",
7473
"@vueuse/router": "catalog:frontend",
75-
"codemirror": "catalog:frontend",
76-
"codemirror-theme-vars": "catalog:frontend",
7774
"comlink": "catalog:frontend",
7875
"d3": "catalog:frontend",
7976
"d3-hierarchy": "catalog:frontend",
8077
"diff-match-patch-es": "catalog:frontend",
8178
"floating-vue": "catalog:frontend",
8279
"fuse.js": "catalog:frontend",
8380
"idb-keyval": "catalog:frontend",
81+
"modern-monaco": "catalog:frontend",
8482
"nanovis": "catalog:frontend",
8583
"splitpanes": "catalog:frontend",
8684
"stream-json": "catalog:inlined",

packages/rolldown/src/app/components/code/DiffEditor.vue

Lines changed: 206 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
<script setup lang="ts">
2-
import type CodeMirror from 'codemirror'
2+
import type * as Monaco from 'modern-monaco/editor-core'
3+
import { isDark } from '@vitejs/devtools-ui/composables/dark'
34
import { Pane, Splitpanes } from 'splitpanes'
4-
import { computed, nextTick, onMounted, toRefs, useTemplateRef, watchEffect } from 'vue'
5-
import { guessCodemirrorMode, syncEditorScrolls, syncScrollListeners, useCodeMirror } from '~/composables/codemirror'
5+
import { computed, nextTick, onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
6+
import {
7+
applyMonacoTheme,
8+
createReadOnlyMonacoEditor,
9+
getMonaco,
10+
getMonacoWordWrap,
11+
guessMonacoLanguage,
12+
setModelLanguageIfNeeded,
13+
setupMonacoScrollSync,
14+
syncMonacoEditorScrolls,
15+
} from '~/composables/monaco'
616
import { settings } from '~/state/settings'
717
import { calculateDiffWithWorker } from '~/worker/diff'
818
@@ -13,129 +23,223 @@ const props = defineProps<{
1323
diff: boolean
1424
}>()
1525
16-
const { from, to } = toRefs(props)
17-
1826
const fromEl = useTemplateRef('fromEl')
1927
const toEl = useTemplateRef('toEl')
2028
21-
let cm1: CodeMirror.Editor
22-
let cm2: CodeMirror.Editor
23-
24-
onMounted(() => {
25-
cm1 = useCodeMirror(
26-
fromEl,
27-
from,
28-
{
29-
mode: 'javascript',
30-
readOnly: true,
31-
lineNumbers: true,
32-
},
33-
)
34-
35-
cm2 = useCodeMirror(
36-
toEl,
37-
to,
38-
{
39-
mode: 'javascript',
40-
readOnly: true,
41-
lineNumbers: true,
42-
},
43-
)
44-
45-
syncScrollListeners(cm1, cm2)
46-
47-
watchEffect(() => {
48-
cm1.setOption('lineWrapping', settings.value.codeviewerLineWrap)
49-
cm2.setOption('lineWrapping', settings.value.codeviewerLineWrap)
50-
})
29+
let monaco: typeof Monaco | null = null
30+
let fromEditor: Monaco.editor.IStandaloneCodeEditor | null = null
31+
let toEditor: Monaco.editor.IStandaloneCodeEditor | null = null
32+
let fromModel: Monaco.editor.ITextModel | null = null
33+
let toModel: Monaco.editor.ITextModel | null = null
34+
let fromDecorations: Monaco.editor.IEditorDecorationsCollection | null = null
35+
let toDecorations: Monaco.editor.IEditorDecorationsCollection | null = null
36+
let disposeScrollSync: (() => void) | null = null
37+
let diffVersion = 0
5138
52-
watchEffect(async () => {
53-
cm1.getWrapperElement().style.display = props.oneColumn ? 'none' : ''
54-
if (!props.oneColumn) {
55-
await nextTick()
56-
// Force sync to current scroll
57-
cm1.refresh()
58-
syncEditorScrolls(cm2, cm1)
59-
}
60-
})
39+
function setModelValue(model: Monaco.editor.ITextModel, value: string) {
40+
if (model.getValue() !== value)
41+
model.setValue(value)
42+
}
6143
62-
watchEffect(async () => {
63-
const l = from.value
64-
const r = to.value
65-
const diffEnabled = props.diff
44+
function applyDiffDecorations(changes: Array<[number, string]>) {
45+
if (!monaco || !fromModel || !toModel || !fromDecorations || !toDecorations)
46+
return
6647
67-
cm1.setOption('mode', guessCodemirrorMode(l))
68-
cm2.setOption('mode', guessCodemirrorMode(r))
48+
const fromEntries: Monaco.editor.IModelDeltaDecoration[] = []
49+
const toEntries: Monaco.editor.IModelDeltaDecoration[] = []
6950
70-
await nextTick()
51+
const addedLines = new Set<number>()
52+
const removedLines = new Set<number>()
7153
72-
cm1.startOperation()
73-
cm2.startOperation()
74-
75-
// clean up marks
76-
cm1.getAllMarks().forEach(i => i.clear())
77-
cm2.getAllMarks().forEach(i => i.clear())
78-
for (let i = 0; i < cm1.lineCount() + 2; i++)
79-
cm1.removeLineClass(i, 'background', 'diff-removed')
80-
for (let i = 0; i < cm2.lineCount() + 2; i++)
81-
cm2.removeLineClass(i, 'background', 'diff-added')
82-
83-
if (diffEnabled && from.value) {
84-
const changes = await calculateDiffWithWorker(l, r)
85-
86-
const addedLines = new Set()
87-
const removedLines = new Set()
88-
89-
let indexL = 0
90-
let indexR = 0
91-
changes.forEach(([type, change]) => {
92-
if (type === 1) {
93-
const start = cm2.posFromIndex(indexR)
94-
indexR += change.length
95-
const end = cm2.posFromIndex(indexR)
96-
cm2.markText(start, end, { className: 'diff-added-inline' })
97-
for (let i = start.line; i <= end.line; i++) addedLines.add(i)
98-
}
99-
else if (type === -1) {
100-
const start = cm1.posFromIndex(indexL)
101-
indexL += change.length
102-
const end = cm1.posFromIndex(indexL)
103-
cm1.markText(start, end, { className: 'diff-removed-inline' })
104-
for (let i = start.line; i <= end.line; i++) removedLines.add(i)
105-
}
106-
else {
107-
indexL += change.length
108-
indexR += change.length
109-
}
110-
})
111-
112-
Array.from(removedLines).forEach(i =>
113-
cm1.addLineClass(i, 'background', 'diff-removed'),
114-
)
115-
Array.from(addedLines).forEach(i =>
116-
cm2.addLineClass(i, 'background', 'diff-added'),
117-
)
54+
let fromIndex = 0
55+
let toIndex = 0
56+
57+
for (const [type, change] of changes) {
58+
if (type === 1) {
59+
const start = toModel.getPositionAt(toIndex)
60+
toIndex += change.length
61+
const end = toModel.getPositionAt(toIndex)
62+
63+
if (start.lineNumber !== end.lineNumber || start.column !== end.column) {
64+
toEntries.push({
65+
range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column),
66+
options: {
67+
inlineClassName: 'diff-added-inline',
68+
},
69+
})
70+
}
71+
72+
for (let i = start.lineNumber; i <= end.lineNumber; i++)
73+
addedLines.add(i)
74+
}
75+
else if (type === -1) {
76+
const start = fromModel.getPositionAt(fromIndex)
77+
fromIndex += change.length
78+
const end = fromModel.getPositionAt(fromIndex)
79+
80+
if (start.lineNumber !== end.lineNumber || start.column !== end.column) {
81+
fromEntries.push({
82+
range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column),
83+
options: {
84+
inlineClassName: 'diff-removed-inline',
85+
},
86+
})
87+
}
88+
89+
for (let i = start.lineNumber; i <= end.lineNumber; i++)
90+
removedLines.add(i)
91+
}
92+
else {
93+
fromIndex += change.length
94+
toIndex += change.length
11895
}
96+
}
11997
120-
cm1.endOperation()
121-
cm2.endOperation()
98+
for (const line of removedLines) {
99+
fromEntries.push({
100+
range: new monaco.Range(line, 1, line, 1),
101+
options: {
102+
className: 'diff-removed',
103+
isWholeLine: true,
104+
},
105+
})
106+
}
107+
108+
for (const line of addedLines) {
109+
toEntries.push({
110+
range: new monaco.Range(line, 1, line, 1),
111+
options: {
112+
className: 'diff-added',
113+
isWholeLine: true,
114+
},
115+
})
116+
}
117+
118+
fromDecorations.set(fromEntries)
119+
toDecorations.set(toEntries)
120+
}
121+
122+
onMounted(async () => {
123+
if (!fromEl.value || !toEl.value)
124+
return
125+
126+
monaco = await getMonaco()
127+
128+
fromEditor = createReadOnlyMonacoEditor(monaco, fromEl.value, {
129+
wordWrap: getMonacoWordWrap(settings.value.codeviewerLineWrap),
122130
})
131+
toEditor = createReadOnlyMonacoEditor(monaco, toEl.value, {
132+
wordWrap: getMonacoWordWrap(settings.value.codeviewerLineWrap),
133+
})
134+
135+
fromModel = monaco.editor.createModel(props.from, guessMonacoLanguage(props.from))
136+
toModel = monaco.editor.createModel(props.to, guessMonacoLanguage(props.to))
137+
138+
fromEditor.setModel(fromModel)
139+
toEditor.setModel(toModel)
140+
141+
fromDecorations = fromEditor.createDecorationsCollection()
142+
toDecorations = toEditor.createDecorationsCollection()
143+
144+
disposeScrollSync = setupMonacoScrollSync(fromEditor, toEditor)
145+
146+
applyMonacoTheme(monaco)
147+
148+
if (!props.oneColumn)
149+
syncMonacoEditorScrolls(toEditor, fromEditor)
150+
151+
await updateDiffDecorations(props.from, props.to, props.diff)
152+
})
153+
154+
watch(
155+
() => settings.value.codeviewerLineWrap,
156+
(enabled) => {
157+
const wordWrap = getMonacoWordWrap(enabled)
158+
fromEditor?.updateOptions({ wordWrap })
159+
toEditor?.updateOptions({ wordWrap })
160+
},
161+
{ immediate: true },
162+
)
163+
164+
watch(isDark, () => {
165+
if (monaco)
166+
applyMonacoTheme(monaco)
123167
})
124168
169+
watch(
170+
() => props.oneColumn,
171+
async (oneColumn) => {
172+
if (!fromEditor || !toEditor)
173+
return
174+
175+
fromEl.value!.style.display = oneColumn ? 'none' : ''
176+
177+
await nextTick()
178+
179+
fromEditor.layout()
180+
toEditor.layout()
181+
182+
if (!oneColumn)
183+
syncMonacoEditorScrolls(toEditor, fromEditor)
184+
},
185+
{ immediate: true },
186+
)
187+
188+
async function updateDiffDecorations(from: string, to: string, diffEnabled: boolean) {
189+
if (!monaco || !fromModel || !toModel || !fromDecorations || !toDecorations)
190+
return
191+
192+
const currentVersion = ++diffVersion
193+
194+
setModelValue(fromModel, from)
195+
setModelValue(toModel, to)
196+
197+
setModelLanguageIfNeeded(monaco, fromModel, guessMonacoLanguage(from))
198+
setModelLanguageIfNeeded(monaco, toModel, guessMonacoLanguage(to))
199+
200+
fromDecorations.set([])
201+
toDecorations.set([])
202+
203+
if (!diffEnabled || !from || from === to)
204+
return
205+
206+
const changes = await calculateDiffWithWorker(from, to)
207+
if (currentVersion !== diffVersion)
208+
return
209+
210+
applyDiffDecorations(changes)
211+
}
212+
213+
watch(
214+
() => [props.from, props.to, props.diff] as const,
215+
([from, to, diffEnabled]) => {
216+
updateDiffDecorations(from, to, diffEnabled)
217+
},
218+
)
219+
125220
const leftPanelSize = computed(() => {
126221
return props.oneColumn
127222
? 0
128223
: settings.value.codeviewerDiffPanelSize
129224
})
130225
131226
function onUpdate(size: number) {
132-
// Refresh sizes
133-
cm1?.refresh()
134-
cm2?.refresh()
227+
fromEditor?.layout()
228+
toEditor?.layout()
229+
135230
if (props.oneColumn)
136231
return
232+
137233
settings.value.codeviewerDiffPanelSize = size
138234
}
235+
236+
onBeforeUnmount(() => {
237+
disposeScrollSync?.()
238+
fromEditor?.dispose()
239+
toEditor?.dispose()
240+
fromModel?.dispose()
241+
toModel?.dispose()
242+
})
139243
</script>
140244

141245
<template>

0 commit comments

Comments
 (0)