@@ -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,6 +18,9 @@ const code = ref('')
17
18
const serverCode = shallowRef <string | undefined >(undefined )
18
19
const draft = ref (false )
19
20
const loading = ref (true )
21
+ // finished.value is true when saving the file, we need it to restore the caret position
22
+ const saving = ref (true )
23
+ const currentPosition = ref <CodeMirror .Position | undefined >()
20
24
21
25
watch (
22
26
() => props .file ,
@@ -34,10 +38,15 @@ watch(
34
38
serverCode .value = code .value
35
39
draft .value = false
36
40
}
37
- finally {
38
- // fire focusing editor after loading
39
- nextTick (() => (loading .value = false ))
41
+ catch (e ) {
42
+ console .error (' cannot fetch file' , e )
40
43
}
44
+
45
+ await nextTick ()
46
+
47
+ // fire focusing editor after loading
48
+ loading .value = false
49
+ saving .value = false
41
50
},
42
51
{ immediate: true },
43
52
)
@@ -65,13 +74,9 @@ watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFil
65
74
const ext = computed (() => props .file ?.filepath ?.split (/ \. / g ).pop () || ' js' )
66
75
const editor = ref <any >()
67
76
68
- const cm = computed <CodeMirror .EditorFromTextArea | undefined >(
69
- () => editor .value ?.cm ,
70
- )
71
77
const failed = computed (
72
78
() => props .file ?.tasks .filter (i => i .result ?.state === ' fail' ) || [],
73
79
)
74
-
75
80
const widgets: CodeMirror .LineWidget [] = []
76
81
const handles: CodeMirror .LineHandle [] = []
77
82
const listeners: [el : HTMLSpanElement , l : EventListener , t : () => void ][] = []
@@ -134,54 +139,111 @@ function createErrorElement(e: ErrorWithDiff) {
134
139
const el: EventListener = async () => {
135
140
await openInEditor (stack .file , stack .line , stack .column )
136
141
}
142
+ span .addEventListener (' click' , el )
137
143
div .appendChild (span )
138
144
listeners .push ([span , el , () => destroyTooltip (span )])
139
145
handles .push (codemirrorRef .value ! .addLineClass (stack .line - 1 , ' wrap' , ' bg-red-500/10' ))
140
146
widgets .push (codemirrorRef .value ! .addLineWidget (stack .line - 1 , div ))
141
147
}
142
148
143
- watch (
144
- [cm , failed ] ,
145
- ([cmValue ]) => {
149
+ const { pause, resume } = watch (
150
+ [codemirrorRef , failed , finished , saving ] as const ,
151
+ ([cmValue , f , end , s ]) => {
146
152
if (! cmValue ) {
153
+ widgets .length = 0
154
+ handles .length = 0
147
155
clearListeners ()
148
156
return
149
157
}
150
158
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
159
+ // if still running or saving return
160
+ if (! end || s ) {
161
+ return
162
+ }
157
163
158
- cmValue .on (' changes' , codemirrorChanges )
164
+ cmValue .off (' changes' , codemirrorChanges )
159
165
160
- failed .value .forEach ((i ) => {
161
- i .result ?.errors ?.forEach (createErrorElement )
162
- })
163
- if (! hasBeenEdited .value ) {
164
- cmValue .clearHistory ()
165
- } // Prevent getting access to initial state
166
- }, 100 )
166
+ // cleanup previous data
167
+ clearListeners ()
168
+ widgets .forEach (widget => widget .clear ())
169
+ handles .forEach (h => cmValue ?.removeLineClass (h , ' wrap' ))
170
+ widgets .length = 0
171
+ handles .length = 0
172
+
173
+ // add new data
174
+ f .forEach ((i ) => {
175
+ i .result ?.errors ?.forEach (createErrorElement )
176
+ })
177
+
178
+ // Prevent getting access to initial state
179
+ if (! hasBeenEdited .value ) {
180
+ cmValue .clearHistory ()
181
+ }
182
+
183
+ cmValue .on (' changes' , codemirrorChanges )
184
+
185
+ // restore caret position
186
+ const { ch, line } = currentPosition .value ?? {}
187
+ console .log (' WTF: ' , currentPosition .value )
188
+ console .error (' WTF' , new Error (' WTF' ))
189
+ if (typeof ch === ' number' && typeof line === ' number' ) {
190
+ currentPosition .value = undefined
191
+ }
167
192
},
168
193
{ flush: ' post' },
169
194
)
170
195
196
+ watchDebounced (() => [finished .value , saving .value , currentPosition .value ] as const , ([f , s ], old ) => {
197
+ if (f && ! s && old && old [2 ]) {
198
+ codemirrorRef .value ?.setCursor (old [2 ])
199
+ }
200
+ }, { debounce: 100 , flush: ' post' })
201
+
171
202
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
203
+ if (saving .value ) {
204
+ return
205
+ }
206
+ pause ()
207
+ saving .value = true
208
+ await nextTick ()
209
+ try {
210
+ currentPosition .value = codemirrorRef .value ?.getCursor ()
211
+ hasBeenEdited .value = true
212
+ // save the file changes
213
+ await client .rpc .saveTestFile (props .file ! .filepath , content )
214
+ // update original server code
215
+ serverCode .value = content
216
+ // update draft indicator in the tab title (</> * Code)
217
+ draft .value = false
218
+ // the server will send 3 events in a row
219
+ // await first change in the state
220
+ await until (finished ).toBe (false , { flush: ' sync' , timeout: 1000 , throwOnTimeout: false })
221
+ // await second change in the state
222
+ await until (finished ).toBe (true , { flush: ' sync' , timeout: 1000 , throwOnTimeout: false })
223
+ // await last change in the state
224
+ await until (finished ).toBe (false , { flush: ' sync' , timeout: 1000 , throwOnTimeout: false })
225
+ }
226
+ catch (e ) {
227
+ console .error (' error saving file' , e )
228
+ }
229
+
230
+ // activate watcher
231
+ resume ()
232
+ await nextTick ()
233
+ // enable adding the errors if present
234
+ saving .value = false
176
235
}
236
+
237
+ // we need to remove listeners before unmounting the component: the watcher will not be called
238
+ onBeforeUnmount (clearListeners )
177
239
</script >
178
240
179
241
<template >
180
242
<CodeMirrorContainer
181
243
ref =" editor"
182
244
v-model =" code"
183
245
h-full
184
- v-bind =" { lineNumbers: true, readOnly: isReport }"
246
+ v-bind =" { lineNumbers: true, readOnly: isReport, saving }"
185
247
:mode =" ext"
186
248
data-testid =" code-mirror"
187
249
@save =" onSave"
0 commit comments