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'
34import { 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'
616import { settings } from ' ~/state/settings'
717import { calculateDiffWithWorker } from ' ~/worker/diff'
818
@@ -13,129 +23,223 @@ const props = defineProps<{
1323 diff: boolean
1424}>()
1525
16- const { from, to } = toRefs (props )
17-
1826const fromEl = useTemplateRef (' fromEl' )
1927const 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+
125220const leftPanelSize = computed (() => {
126221 return props .oneColumn
127222 ? 0
128223 : settings .value .codeviewerDiffPanelSize
129224})
130225
131226function 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