Skip to content

Commit

Permalink
feat(ui): open in IDE tests with error on report and code tabs (#992)
Browse files Browse the repository at this point in the history
  • Loading branch information
userquin committed Mar 21, 2022
1 parent 5f021e4 commit c977c33
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 30 deletions.
31 changes: 2 additions & 29 deletions packages/ui/client/components/views/ViewEditor.vue
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type CodeMirror from 'codemirror'
import { useCodeError } from '../../composables/error'
import { client } from '~/composables/client'
import type { File } from '#types'
Expand All @@ -23,41 +24,13 @@ 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 hasBeenEdited = ref(false)
const widgets: CodeMirror.LineWidget[] = []
const handles: CodeMirror.LineHandle[] = []
const { hasBeenEdited } = useCodeError(props, cm, failed)
async function onSave(content: string) {
hasBeenEdited.value = true
await client.rpc.writeFile(props.file!.filepath, content)
}
watch([cm, failed], () => {
if (!cm.value)
return
setTimeout(() => {
widgets.forEach(widget => widget.clear())
handles.forEach(h => cm.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0
failed.value.forEach((i) => {
const e = i.result?.error
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
if (stacks.length) {
const pos = stacks[0].sourcePos || stacks[0]
const el = document.createElement('pre')
el.className = 'c-red-600 dark:c-red-400'
el.textContent = `${' '.repeat(pos.column)}^ ${e?.nameStr}: ${e?.message}`
handles.push(cm.value!.addLineClass(pos.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(pos.line - 1, el))
}
})
if (!hasBeenEdited.value) cm.value?.clearHistory() // Prevent getting access to initial state
}, 100)
}, { flush: 'post' })
</script>

<template>
Expand Down
12 changes: 11 additions & 1 deletion packages/ui/client/components/views/ViewReport.vue
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { openInEditor, shouldOpenInEditor } from '../../composables/error'
import type { File, Task } from '#types'
import { config } from '~/composables/client'
Expand Down Expand Up @@ -36,7 +37,16 @@ function relative(p: string) {
{{ task.name }}
<div v-if="task.result?.error" class="scrolls scrolls-rounded task-error">
<pre><b>{{ task.result.error.name || task.result.error.nameStr }}</b>: {{ task.result.error.message }}</pre>
<pre v-for="stack, i of task.result.error.stacks || []" :key="i" op80> - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}</pre>
<div v-for="({ file: efile, line, column }, i) of task.result.error.stacks || []" :key="i" class="op80 flex gap-x-2 items-center">
<pre> - {{ relative(efile) }}:{{ line }}:{{ column }}</pre>
<div
v-if="shouldOpenInEditor(efile, props.file.name)"
class="i-carbon-launch text-red-900 hover:cursor-pointer"
tabindex="0"
title="Open in IDE"
@click.passive="openInEditor(efile, line, column)"
/>
</div>
</div>
</div>
</div>
Expand Down
75 changes: 75 additions & 0 deletions packages/ui/client/composables/error.ts
@@ -0,0 +1,75 @@
import type { Ref } from 'vue'
import type CodeMirror from 'codemirror'
import type { File, Task } from '#types'

export function shouldOpenInEditor(name: string, fileName?: string) {
return fileName && name.endsWith(fileName)
}

export async function openInEditor(name: string, line: number, column: number) {
const url = encodeURI(`${name}:${line}:${column}`)
await fetch(`/__open-in-editor?file=${url}`)
}

export function useCodeError(
props: Readonly<{ file?: File | undefined }>,
cm: Ref<CodeMirror.EditorFromTextArea | undefined>,
failed: Ref<Task[]>,
) {
const widgets: CodeMirror.LineWidget[] = []
const handles: CodeMirror.LineHandle[] = []
const listeners: [el: HTMLSpanElement, l: EventListenerOrEventListenerObject][] = []

const hasBeenEdited = ref(false)

const clearListeners = () => {
listeners.forEach(([el, l]) => {
el.removeEventListener('click', l)
})
listeners.length = 0
}

watch([cm, failed], () => {
if (!cm.value) {
clearListeners()
return
}

setTimeout(() => {
clearListeners()
widgets.forEach(widget => widget.clear())
handles.forEach(h => cm.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0

failed.value.forEach((i) => {
const e = i.result?.error
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
if (stacks.length) {
const pos = stacks[0].sourcePos || stacks[0]
const div = document.createElement('div')
div.className = 'op80 flex gap-x-2 items-center'
const pre = document.createElement('pre')
pre.className = 'c-red-600 dark:c-red-400'
pre.textContent = `${' '.repeat(pos.column)}^ ${e?.nameStr}: ${e?.message}`
div.appendChild(pre)
const span = document.createElement('span')
span.className = 'i-carbon-launch text-red-900 hover:cursor-pointer'
span.tabIndex = 0
span.title = 'Open in IDE'
const el: EventListenerOrEventListenerObject = async() => {
await openInEditor(stacks[0].file, pos.line, pos.column)
}
listeners.push([span, el])
span.addEventListener('click', el)
div.appendChild(span)
handles.push(cm.value!.addLineClass(pos.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(pos.line - 1, div))
}
})
if (!hasBeenEdited.value) cm.value?.clearHistory() // Prevent getting access to initial state
}, 100)
}, { flush: 'post' })

return { hasBeenEdited }
}

0 comments on commit c977c33

Please sign in to comment.