@@ -3,6 +3,7 @@ import type CodeMirror from 'codemirror'
3
3
import type { ErrorWithDiff , File } from ' vitest'
4
4
import { createTooltip , destroyTooltip } from ' floating-vue'
5
5
import { client , isReport } from ' ~/composables/client'
6
+ import { finished } from ' ~/composables/client/state'
6
7
import { codemirrorRef } from ' ~/composables/codemirror'
7
8
import { openInEditor } from ' ~/composables/error'
8
9
import { lineNumber } from ' ~/composables/params'
@@ -17,41 +18,60 @@ const code = ref('')
17
18
const serverCode = shallowRef <string | undefined >(undefined )
18
19
const draft = ref (false )
19
20
const loading = ref (true )
21
+ const saving = ref (false )
22
+ const currentPosition = ref <CodeMirror .Position | undefined >()
20
23
21
24
watch (
22
25
() => props .file ,
23
26
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
+ }
24
32
loading .value = true
25
33
try {
26
34
if (! props .file || ! props .file ?.filepath ) {
27
35
code .value = ' '
28
36
serverCode .value = code .value
29
37
draft .value = false
38
+ loading .value = false
30
39
return
31
40
}
32
41
33
42
code .value = (await client .rpc .readTestFile (props .file .filepath )) || ' '
34
43
serverCode .value = code .value
35
44
draft .value = false
36
45
}
37
- finally {
38
- // fire focusing editor after loading
39
- nextTick (() => (loading .value = false ))
46
+ catch (e ) {
47
+ console .error (' cannot fetch file' , e )
40
48
}
49
+
50
+ await nextTick ()
51
+
52
+ // fire focusing editor after loading
53
+ loading .value = false
41
54
},
42
55
{ immediate: true },
43
56
)
44
57
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 ) {
47
60
if (l != null ) {
48
61
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
+ }
55
75
})
56
76
}
57
77
else {
@@ -65,13 +85,9 @@ watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFil
65
85
const ext = computed (() => props .file ?.filepath ?.split (/ \. / g ).pop () || ' js' )
66
86
const editor = ref <any >()
67
87
68
- const cm = computed <CodeMirror .EditorFromTextArea | undefined >(
69
- () => editor .value ?.cm ,
70
- )
71
88
const failed = computed (
72
89
() => props .file ?.tasks .filter (i => i .result ?.state === ' fail' ) || [],
73
90
)
74
-
75
91
const widgets: CodeMirror .LineWidget [] = []
76
92
const handles: CodeMirror .LineHandle [] = []
77
93
const listeners: [el : HTMLSpanElement , l : EventListener , t : () => void ][] = []
@@ -134,54 +150,144 @@ function createErrorElement(e: ErrorWithDiff) {
134
150
const el: EventListener = async () => {
135
151
await openInEditor (stack .file , stack .line , stack .column )
136
152
}
153
+ span .addEventListener (' click' , el )
137
154
div .appendChild (span )
138
155
listeners .push ([span , el , () => destroyTooltip (span )])
139
156
handles .push (codemirrorRef .value ! .addLineClass (stack .line - 1 , ' wrap' , ' bg-red-500/10' ))
140
157
widgets .push (codemirrorRef .value ! .addLineWidget (stack .line - 1 , div ))
141
158
}
142
159
143
- watch (
144
- [cm , failed ] ,
145
- ([cmValue ]) => {
160
+ const { pause, resume } = watch (
161
+ [codemirrorRef , failed , finished ] as const ,
162
+ ([cmValue , f , end ]) => {
146
163
if (! cmValue ) {
164
+ widgets .length = 0
165
+ handles .length = 0
147
166
clearListeners ()
148
167
return
149
168
}
150
169
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
+ }
157
174
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
159
184
160
- failed .value .forEach ((i ) => {
185
+ setTimeout (() => {
186
+ // add new data
187
+ f .forEach ((i ) => {
161
188
i .result ?.errors ?.forEach (createErrorElement )
162
189
})
190
+
191
+ // Prevent getting access to initial state
163
192
if (! hasBeenEdited .value ) {
164
193
cmValue .clearHistory ()
165
- } // Prevent getting access to initial state
194
+ }
195
+
196
+ cmValue .on (' changes' , codemirrorChanges )
166
197
}, 100 )
167
198
},
168
199
{ flush: ' post' },
169
200
)
170
201
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
+
171
208
async 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 ()
176
279
}
280
+
281
+ // we need to remove listeners before unmounting the component: the watcher will not be called
282
+ onBeforeUnmount (clearListeners )
177
283
</script >
178
284
179
285
<template >
180
286
<CodeMirrorContainer
181
287
ref =" editor"
182
288
v-model =" code"
183
289
h-full
184
- v-bind =" { lineNumbers: true, readOnly: isReport }"
290
+ v-bind =" { lineNumbers: true, readOnly: isReport, saving }"
185
291
:mode =" ext"
186
292
data-testid =" code-mirror"
187
293
@save =" onSave"
0 commit comments