/
ViewEditor.vue
129 lines (114 loc) · 3.81 KB
/
ViewEditor.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<script setup lang="ts">
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type CodeMirror from 'codemirror'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { openInEditor } from '../../composables/error'
import { client } from '~/composables/client'
import type { File } from '#types'
const props = defineProps<{
file?: File
}>()
const emit = defineEmits<{ (event: 'draft', value: boolean): void }>()
const code = ref('')
const serverCode = shallowRef<string | undefined>(undefined)
const draft = ref(false)
watch(() => props.file,
async () => {
if (!props.file || !props.file?.filepath) {
code.value = ''
serverCode.value = code.value
draft.value = false
return
}
code.value = await client.rpc.readFile(props.file.filepath)
serverCode.value = code.value
draft.value = false
},
{ immediate: true },
)
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][] = []
const hasBeenEdited = ref(false)
const clearListeners = () => {
listeners.forEach(([el, l, t]) => {
el.removeEventListener('click', l)
t()
})
listeners.length = 0
}
useResizeObserver(editor, () => {
cm.value?.refresh()
})
function codemirrorChanges() {
draft.value = serverCode.value !== cm.value!.getValue()
}
watch(draft, (d) => {
emit('draft', d)
}, { immediate: true })
watch([cm, failed], ([cmValue]) => {
if (!cmValue) {
clearListeners()
return
}
setTimeout(() => {
clearListeners()
widgets.forEach(widget => widget.clear())
handles.forEach(h => cm.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0
cmValue.on('changes', codemirrorChanges)
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 stack = 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(stack.column)}^ ${e?.nameStr}: ${e?.message}`
div.appendChild(pre)
const span = document.createElement('span')
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
span.tabIndex = 0
span.ariaLabel = 'Open in Editor'
const tooltip = createTooltip(span, {
content: 'Open in Editor',
placement: 'bottom',
}, false)
const el: EventListener = async () => {
await openInEditor(stacks[0].file, stack.line, stack.column)
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
}
})
if (!hasBeenEdited.value)
cmValue.clearHistory() // Prevent getting access to initial state
}, 100)
}, { flush: 'post' })
async function onSave(content: string) {
hasBeenEdited.value = true
await client.rpc.writeFile(props.file!.filepath, content)
serverCode.value = content
draft.value = false
}
</script>
<template>
<CodeMirror
ref="editor"
v-model="code"
h-full
v-bind="{ lineNumbers: true }"
:mode="ext"
data-testid="code-mirror"
@save="onSave"
/>
</template>