Skip to content

Commit

Permalink
fix(ui): add errors and draft state (*) to the code editor
Browse files Browse the repository at this point in the history
  • Loading branch information
userquin committed Dec 7, 2024
1 parent 3c18ecf commit 29bcbeb
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/ui/client/components/CodeMirrorContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { codemirrorRef } from '~/composables/codemirror'
const { mode, readOnly } = defineProps<{
mode?: string
readOnly?: boolean
saving?: boolean
}>()
const emit = defineEmits<{
Expand Down Expand Up @@ -53,7 +54,7 @@ onMounted(async () => {
</script>

<template>
<div relative font-mono text-sm class="codemirror-scrolls">
<div relative font-mono text-sm class="codemirror-scrolls" :class="saving ? 'codemirror-busy' : undefined">
<textarea ref="el" />
</div>
</template>
120 changes: 91 additions & 29 deletions packages/ui/client/components/views/ViewEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type CodeMirror from 'codemirror'
import type { ErrorWithDiff, File } from 'vitest'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { client, isReport } from '~/composables/client'
import { finished } from '~/composables/client/state'
import { codemirrorRef } from '~/composables/codemirror'
import { openInEditor } from '~/composables/error'
import { lineNumber } from '~/composables/params'
Expand All @@ -17,6 +18,9 @@ const code = ref('')
const serverCode = shallowRef<string | undefined>(undefined)
const draft = ref(false)
const loading = ref(true)
// finished.value is true when saving the file, we need it to restore the caret position
const saving = ref(true)
const currentPosition = ref<CodeMirror.Position | undefined>()
watch(
() => props.file,
Expand All @@ -34,10 +38,15 @@ watch(
serverCode.value = code.value
draft.value = false
}
finally {
// fire focusing editor after loading
nextTick(() => (loading.value = false))
catch (e) {
console.error('cannot fetch file', e)
}
await nextTick()
// fire focusing editor after loading
loading.value = false
saving.value = false
},
{ immediate: true },
)
Expand Down Expand Up @@ -65,13 +74,9 @@ watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFil
const ext = computed(() => props.file?.filepath?.split(/\./g).pop() || 'js')
const editor = ref<any>()
const cm = computed<CodeMirror.EditorFromTextArea | undefined>(
() => editor.value?.cm,
)
const failed = computed(
() => props.file?.tasks.filter(i => i.result?.state === 'fail') || [],
)
const widgets: CodeMirror.LineWidget[] = []
const handles: CodeMirror.LineHandle[] = []
const listeners: [el: HTMLSpanElement, l: EventListener, t: () => void][] = []
Expand Down Expand Up @@ -134,54 +139,111 @@ function createErrorElement(e: ErrorWithDiff) {
const el: EventListener = async () => {
await openInEditor(stack.file, stack.line, stack.column)
}
span.addEventListener('click', el)
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(codemirrorRef.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(codemirrorRef.value!.addLineWidget(stack.line - 1, div))
}
watch(
[cm, failed],
([cmValue]) => {
const { pause, resume } = watch(
[codemirrorRef, failed, finished, saving] as const,
([cmValue, f, end, s]) => {
if (!cmValue) {
widgets.length = 0
handles.length = 0
clearListeners()
return
}
setTimeout(() => {
clearListeners()
widgets.forEach(widget => widget.clear())
handles.forEach(h => codemirrorRef.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0
// if still running or saving return
if (!end || s) {
return
}
cmValue.on('changes', codemirrorChanges)
cmValue.off('changes', codemirrorChanges)
failed.value.forEach((i) => {
i.result?.errors?.forEach(createErrorElement)
})
if (!hasBeenEdited.value) {
cmValue.clearHistory()
} // Prevent getting access to initial state
}, 100)
// cleanup previous data
clearListeners()
widgets.forEach(widget => widget.clear())
handles.forEach(h => cmValue?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0
// add new data
f.forEach((i) => {
i.result?.errors?.forEach(createErrorElement)
})
// Prevent getting access to initial state
if (!hasBeenEdited.value) {
cmValue.clearHistory()
}
cmValue.on('changes', codemirrorChanges)
// restore caret position
const { ch, line } = currentPosition.value ?? {}
console.log('WTF: ', currentPosition.value)

Check failure on line 187 in packages/ui/client/components/views/ViewEditor.vue

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Unexpected console statement
console.error('WTF', new Error('WTF'))
if (typeof ch === 'number' && typeof line === 'number') {
currentPosition.value = undefined
}
},
{ flush: 'post' },
)
watchDebounced(() => [finished.value, saving.value, currentPosition.value] as const, ([f, s], old) => {
if (f && !s && old && old[2]) {
codemirrorRef.value?.setCursor(old[2])
}
}, { debounce: 100, flush: 'post' })
async function onSave(content: string) {
hasBeenEdited.value = true
await client.rpc.saveTestFile(props.file!.filepath, content)
serverCode.value = content
draft.value = false
if (saving.value) {
return
}
pause()
saving.value = true
await nextTick()
try {
currentPosition.value = codemirrorRef.value?.getCursor()
hasBeenEdited.value = true
// save the file changes
await client.rpc.saveTestFile(props.file!.filepath, content)
// update original server code
serverCode.value = content
// update draft indicator in the tab title (</> * Code)
draft.value = false
// the server will send 3 events in a row
// await first change in the state
await until(finished).toBe(false, { flush: 'sync', timeout: 1000, throwOnTimeout: false })
// await second change in the state
await until(finished).toBe(true, { flush: 'sync', timeout: 1000, throwOnTimeout: false })
// await last change in the state
await until(finished).toBe(false, { flush: 'sync', timeout: 1000, throwOnTimeout: false })
}
catch (e) {
console.error('error saving file', e)
}
// activate watcher
resume()
await nextTick()
// enable adding the errors if present
saving.value = false
}
// we need to remove listeners before unmounting the component: the watcher will not be called
onBeforeUnmount(clearListeners)
</script>

<template>
<CodeMirrorContainer
ref="editor"
v-model="code"
h-full
v-bind="{ lineNumbers: true, readOnly: isReport }"
v-bind="{ lineNumbers: true, readOnly: isReport, saving }"
:mode="ext"
data-testid="code-mirror"
@save="onSave"
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/client/composables/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export const client = (function createVitestClient() {
},
onFinished(_files, errors) {
explorerTree.endRun()
testRunState.value = 'idle'
// don't change the testRunState.value here:
// - when saving the file in the codemirror requires explorer tree endRun to finish (multiple microtasks)
// - if we change here the state before the tasks states are updated, the cursor position will be lost
// - line moved to composables/explorer/collector.ts::refreshExplorer after calling updateRunningTodoTests
// testRunState.value = 'idle'
unhandledErrors.value = (errors || []).map(parseError)
},
onFinishedReportCoverage() {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/client/composables/explorer/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isTestCase } from '@vitest/runner/utils'
import { toArray } from '@vitest/utils'
import { hasFailedSnapshot } from '@vitest/ws-client'
import { client, findById } from '~/composables/client'
import { testRunState } from '~/composables/client/state'
import { expandNodesOnEndRun } from '~/composables/explorer/expand'
import { runFilter, testMatcher } from '~/composables/explorer/filter'
import { explorerTree } from '~/composables/explorer/index'
Expand Down Expand Up @@ -234,6 +235,7 @@ function refreshExplorer(search: string, filter: Filter, end: boolean) {
// update only at the end
if (end) {
updateRunningTodoTests()
testRunState.value = 'idle'
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/ui/client/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,7 @@ html.dark {
.v-popper__popper .v-popper__arrow-outer {
border-color: var(--background-color);
}

.codemirror-busy > .CodeMirror > .CodeMirror-scroll > .CodeMirror-sizer .CodeMirror-lines {
cursor: wait !important;
}

0 comments on commit 29bcbeb

Please sign in to comment.