Skip to content

Commit 29bcbeb

Browse files
committed
fix(ui): add errors and draft state (*) to the code editor
1 parent 3c18ecf commit 29bcbeb

File tree

5 files changed

+104
-31
lines changed

5 files changed

+104
-31
lines changed

packages/ui/client/components/CodeMirrorContainer.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { codemirrorRef } from '~/composables/codemirror'
44
const { mode, readOnly } = defineProps<{
55
mode?: string
66
readOnly?: boolean
7+
saving?: boolean
78
}>()
89
910
const emit = defineEmits<{
@@ -53,7 +54,7 @@ onMounted(async () => {
5354
</script>
5455

5556
<template>
56-
<div relative font-mono text-sm class="codemirror-scrolls">
57+
<div relative font-mono text-sm class="codemirror-scrolls" :class="saving ? 'codemirror-busy' : undefined">
5758
<textarea ref="el" />
5859
</div>
5960
</template>

packages/ui/client/components/views/ViewEditor.vue

Lines changed: 91 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type CodeMirror from 'codemirror'
33
import type { ErrorWithDiff, File } from 'vitest'
44
import { createTooltip, destroyTooltip } from 'floating-vue'
55
import { client, isReport } from '~/composables/client'
6+
import { finished } from '~/composables/client/state'
67
import { codemirrorRef } from '~/composables/codemirror'
78
import { openInEditor } from '~/composables/error'
89
import { lineNumber } from '~/composables/params'
@@ -17,6 +18,9 @@ const code = ref('')
1718
const serverCode = shallowRef<string | undefined>(undefined)
1819
const draft = ref(false)
1920
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>()
2024
2125
watch(
2226
() => props.file,
@@ -34,10 +38,15 @@ watch(
3438
serverCode.value = code.value
3539
draft.value = false
3640
}
37-
finally {
38-
// fire focusing editor after loading
39-
nextTick(() => (loading.value = false))
41+
catch (e) {
42+
console.error('cannot fetch file', e)
4043
}
44+
45+
await nextTick()
46+
47+
// fire focusing editor after loading
48+
loading.value = false
49+
saving.value = false
4150
},
4251
{ immediate: true },
4352
)
@@ -65,13 +74,9 @@ watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFil
6574
const ext = computed(() => props.file?.filepath?.split(/\./g).pop() || 'js')
6675
const editor = ref<any>()
6776
68-
const cm = computed<CodeMirror.EditorFromTextArea | undefined>(
69-
() => editor.value?.cm,
70-
)
7177
const failed = computed(
7278
() => props.file?.tasks.filter(i => i.result?.state === 'fail') || [],
7379
)
74-
7580
const widgets: CodeMirror.LineWidget[] = []
7681
const handles: CodeMirror.LineHandle[] = []
7782
const listeners: [el: HTMLSpanElement, l: EventListener, t: () => void][] = []
@@ -134,54 +139,111 @@ function createErrorElement(e: ErrorWithDiff) {
134139
const el: EventListener = async () => {
135140
await openInEditor(stack.file, stack.line, stack.column)
136141
}
142+
span.addEventListener('click', el)
137143
div.appendChild(span)
138144
listeners.push([span, el, () => destroyTooltip(span)])
139145
handles.push(codemirrorRef.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
140146
widgets.push(codemirrorRef.value!.addLineWidget(stack.line - 1, div))
141147
}
142148
143-
watch(
144-
[cm, failed],
145-
([cmValue]) => {
149+
const { pause, resume } = watch(
150+
[codemirrorRef, failed, finished, saving] as const,
151+
([cmValue, f, end, s]) => {
146152
if (!cmValue) {
153+
widgets.length = 0
154+
handles.length = 0
147155
clearListeners()
148156
return
149157
}
150158
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+
}
157163
158-
cmValue.on('changes', codemirrorChanges)
164+
cmValue.off('changes', codemirrorChanges)
159165
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+
}
167192
},
168193
{ flush: 'post' },
169194
)
170195
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+
171202
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
176235
}
236+
237+
// we need to remove listeners before unmounting the component: the watcher will not be called
238+
onBeforeUnmount(clearListeners)
177239
</script>
178240

179241
<template>
180242
<CodeMirrorContainer
181243
ref="editor"
182244
v-model="code"
183245
h-full
184-
v-bind="{ lineNumbers: true, readOnly: isReport }"
246+
v-bind="{ lineNumbers: true, readOnly: isReport, saving }"
185247
:mode="ext"
186248
data-testid="code-mirror"
187249
@save="onSave"

packages/ui/client/composables/client/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export const client = (function createVitestClient() {
3232
},
3333
onFinished(_files, errors) {
3434
explorerTree.endRun()
35-
testRunState.value = 'idle'
35+
// don't change the testRunState.value here:
36+
// - when saving the file in the codemirror requires explorer tree endRun to finish (multiple microtasks)
37+
// - if we change here the state before the tasks states are updated, the cursor position will be lost
38+
// - line moved to composables/explorer/collector.ts::refreshExplorer after calling updateRunningTodoTests
39+
// testRunState.value = 'idle'
3640
unhandledErrors.value = (errors || []).map(parseError)
3741
},
3842
onFinishedReportCoverage() {

packages/ui/client/composables/explorer/collector.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isTestCase } from '@vitest/runner/utils'
55
import { toArray } from '@vitest/utils'
66
import { hasFailedSnapshot } from '@vitest/ws-client'
77
import { client, findById } from '~/composables/client'
8+
import { testRunState } from '~/composables/client/state'
89
import { expandNodesOnEndRun } from '~/composables/explorer/expand'
910
import { runFilter, testMatcher } from '~/composables/explorer/filter'
1011
import { explorerTree } from '~/composables/explorer/index'
@@ -234,6 +235,7 @@ function refreshExplorer(search: string, filter: Filter, end: boolean) {
234235
// update only at the end
235236
if (end) {
236237
updateRunningTodoTests()
238+
testRunState.value = 'idle'
237239
}
238240
}
239241

packages/ui/client/styles/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,7 @@ html.dark {
211211
.v-popper__popper .v-popper__arrow-outer {
212212
border-color: var(--background-color);
213213
}
214+
215+
.codemirror-busy > .CodeMirror > .CodeMirror-scroll > .CodeMirror-sizer .CodeMirror-lines {
216+
cursor: wait !important;
217+
}

0 commit comments

Comments
 (0)