@@ -3,6 +3,7 @@ import type CodeMirror from 'codemirror'
33import type { ErrorWithDiff , File } from ' vitest'
44import { createTooltip , destroyTooltip } from ' floating-vue'
55import { client , isReport } from ' ~/composables/client'
6+ import { finished } from ' ~/composables/client/state'
67import { codemirrorRef } from ' ~/composables/codemirror'
78import { openInEditor } from ' ~/composables/error'
89import { lineNumber } from ' ~/composables/params'
@@ -17,41 +18,60 @@ const code = ref('')
1718const serverCode = shallowRef <string | undefined >(undefined )
1819const draft = ref (false )
1920const loading = ref (true )
21+ const saving = ref (false )
22+ const currentPosition = ref <CodeMirror .Position | undefined >()
2023
2124watch (
2225 () => props .file ,
2326 async () => {
27+ // this watcher will be called multiple times when saving the file in the view editor
28+ // since we are saving the file and changing the content inside onSave we just return here
29+ if (saving .value ) {
30+ return
31+ }
2432 loading .value = true
2533 try {
2634 if (! props .file || ! props .file ?.filepath ) {
2735 code .value = ' '
2836 serverCode .value = code .value
2937 draft .value = false
38+ loading .value = false
3039 return
3140 }
3241
3342 code .value = (await client .rpc .readTestFile (props .file .filepath )) || ' '
3443 serverCode .value = code .value
3544 draft .value = false
3645 }
37- finally {
38- // fire focusing editor after loading
39- nextTick (() => (loading .value = false ))
46+ catch (e ) {
47+ console .error (' cannot fetch file' , e )
4048 }
49+
50+ await nextTick ()
51+
52+ // fire focusing editor after loading
53+ loading .value = false
4154 },
4255 { immediate: true },
4356)
4457
45- watch (() => [loading .value , props .file , lineNumber .value ] as const , ([loadingFile , _ , l ]) => {
46- if (! loadingFile ) {
58+ watch (() => [loading .value , saving . value , props .file , lineNumber .value ] as const , ([loadingFile , s , _ , l ]) => {
59+ if (! loadingFile && ! s ) {
4760 if (l != null ) {
4861 nextTick (() => {
49- const line = { line: l ?? 0 , ch: 0 }
50- codemirrorRef .value ?.scrollIntoView (line , 100 )
51- nextTick (() => {
52- codemirrorRef .value ?.focus ()
53- codemirrorRef .value ?.setCursor (line )
54- })
62+ const cp = currentPosition .value
63+ const line = cp ?? { line: l ?? 0 , ch: 0 }
64+ // restore caret position: the watchDebounced below will use old value
65+ if (cp ) {
66+ currentPosition .value = undefined
67+ }
68+ else {
69+ codemirrorRef .value ?.scrollIntoView (line , 100 )
70+ nextTick (() => {
71+ codemirrorRef .value ?.focus ()
72+ codemirrorRef .value ?.setCursor (line )
73+ })
74+ }
5575 })
5676 }
5777 else {
@@ -65,13 +85,9 @@ watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFil
6585const ext = computed (() => props .file ?.filepath ?.split (/ \. / g ).pop () || ' js' )
6686const editor = ref <any >()
6787
68- const cm = computed <CodeMirror .EditorFromTextArea | undefined >(
69- () => editor .value ?.cm ,
70- )
7188const failed = computed (
7289 () => props .file ?.tasks .filter (i => i .result ?.state === ' fail' ) || [],
7390)
74-
7591const widgets: CodeMirror .LineWidget [] = []
7692const handles: CodeMirror .LineHandle [] = []
7793const listeners: [el : HTMLSpanElement , l : EventListener , t : () => void ][] = []
@@ -134,54 +150,144 @@ function createErrorElement(e: ErrorWithDiff) {
134150 const el: EventListener = async () => {
135151 await openInEditor (stack .file , stack .line , stack .column )
136152 }
153+ span .addEventListener (' click' , el )
137154 div .appendChild (span )
138155 listeners .push ([span , el , () => destroyTooltip (span )])
139156 handles .push (codemirrorRef .value ! .addLineClass (stack .line - 1 , ' wrap' , ' bg-red-500/10' ))
140157 widgets .push (codemirrorRef .value ! .addLineWidget (stack .line - 1 , div ))
141158}
142159
143- watch (
144- [cm , failed ] ,
145- ([cmValue ]) => {
160+ const { pause, resume } = watch (
161+ [codemirrorRef , failed , finished ] as const ,
162+ ([cmValue , f , end ]) => {
146163 if (! cmValue ) {
164+ widgets .length = 0
165+ handles .length = 0
147166 clearListeners ()
148167 return
149168 }
150169
151- setTimeout (() => {
152- clearListeners ()
153- widgets .forEach (widget => widget .clear ())
154- handles .forEach (h => codemirrorRef .value ?.removeLineClass (h , ' wrap' ))
155- widgets .length = 0
156- handles .length = 0
170+ // if still running
171+ if (! end ) {
172+ return
173+ }
157174
158- cmValue .on (' changes' , codemirrorChanges )
175+ // cleanup previous data when not saving just reloading
176+ cmValue .off (' changes' , codemirrorChanges )
177+
178+ // cleanup previous data
179+ clearListeners ()
180+ widgets .forEach (widget => widget .clear ())
181+ handles .forEach (h => cmValue ?.removeLineClass (h , ' wrap' ))
182+ widgets .length = 0
183+ handles .length = 0
159184
160- failed .value .forEach ((i ) => {
185+ setTimeout (() => {
186+ // add new data
187+ f .forEach ((i ) => {
161188 i .result ?.errors ?.forEach (createErrorElement )
162189 })
190+
191+ // Prevent getting access to initial state
163192 if (! hasBeenEdited .value ) {
164193 cmValue .clearHistory ()
165- } // Prevent getting access to initial state
194+ }
195+
196+ cmValue .on (' changes' , codemirrorChanges )
166197 }, 100 )
167198 },
168199 { flush: ' post' },
169200)
170201
202+ watchDebounced (() => [finished .value , saving .value , currentPosition .value ] as const , ([f , s ], old ) => {
203+ if (f && ! s && old && old [2 ]) {
204+ codemirrorRef .value ?.setCursor (old [2 ])
205+ }
206+ }, { debounce: 100 , flush: ' post' })
207+
171208async function onSave(content : string ) {
172- hasBeenEdited .value = true
173- await client .rpc .saveTestFile (props .file ! .filepath , content )
174- serverCode .value = content
175- draft .value = false
209+ if (saving .value ) {
210+ return
211+ }
212+ pause ()
213+ saving .value = true
214+ await nextTick ()
215+
216+ // clear previous state
217+ const cmValue = codemirrorRef .value
218+ if (cmValue ) {
219+ cmValue .setOption (' readOnly' , true )
220+ await nextTick ()
221+ cmValue .refresh ()
222+ }
223+ // save cursor position
224+ currentPosition .value = cmValue ?.getCursor ()
225+ cmValue ?.off (' changes' , codemirrorChanges )
226+
227+ // cleanup previous data
228+ clearListeners ()
229+ widgets .forEach (widget => widget .clear ())
230+ handles .forEach (h => cmValue ?.removeLineClass (h , ' wrap' ))
231+ widgets .length = 0
232+ handles .length = 0
233+
234+ try {
235+ hasBeenEdited .value = true
236+ // save the file changes
237+ await client .rpc .saveTestFile (props .file ! .filepath , content )
238+ // update original server code
239+ serverCode .value = content
240+ // update draft indicator in the tab title (</> * Code)
241+ draft .value = false
242+ }
243+ catch (e ) {
244+ console .error (' error saving file' , e )
245+ }
246+
247+ // Prevent getting access to initial state
248+ if (! hasBeenEdited .value ) {
249+ cmValue ?.clearHistory ()
250+ }
251+
252+ try {
253+ // the server will send a few events in a row
254+ // await to re-run test
255+ await until (finished ).toBe (false , { flush: ' sync' , timeout: 1000 , throwOnTimeout: true })
256+ // await to finish
257+ await until (finished ).toBe (true , { flush: ' sync' , timeout: 1000 , throwOnTimeout: false })
258+ }
259+ catch {
260+ // ignore errors
261+ }
262+
263+ // add new data
264+ failed .value .forEach ((i ) => {
265+ i .result ?.errors ?.forEach (createErrorElement )
266+ })
267+
268+ cmValue ?.on (' changes' , codemirrorChanges )
269+
270+ saving .value = false
271+ await nextTick ()
272+ if (cmValue ) {
273+ cmValue .setOption (' readOnly' , false )
274+ await nextTick ()
275+ cmValue .refresh ()
276+ }
277+ // activate watcher
278+ resume ()
176279}
280+
281+ // we need to remove listeners before unmounting the component: the watcher will not be called
282+ onBeforeUnmount (clearListeners )
177283 </script >
178284
179285<template >
180286 <CodeMirrorContainer
181287 ref =" editor"
182288 v-model =" code"
183289 h-full
184- v-bind =" { lineNumbers: true, readOnly: isReport }"
290+ v-bind =" { lineNumbers: true, readOnly: isReport, saving }"
185291 :mode =" ext"
186292 data-testid =" code-mirror"
187293 @save =" onSave"
0 commit comments