Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): add errors and draft state (*) to the code editor #7044

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { 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 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 @@
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 @@
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 @@
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,
userquin marked this conversation as resolved.
Show resolved Hide resolved
([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'))
userquin marked this conversation as resolved.
Show resolved Hide resolved
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 })
userquin marked this conversation as resolved.
Show resolved Hide resolved
}
catch (e) {
console.error('error saving file', e)
userquin marked this conversation as resolved.
Show resolved Hide resolved
}

// 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;
}
Loading