Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego) 183 tests, 76 source files, 9,910 lines of code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
230 lines
6.7 KiB
Vue
230 lines
6.7 KiB
Vue
<template>
|
|
<div class="flex flex-col border-b border-[var(--wraith-border)] bg-[var(--wraith-bg-secondary)]" :style="{ height: editorHeight + 'px' }">
|
|
<!-- Editor toolbar -->
|
|
<div class="flex items-center justify-between px-3 py-1.5 border-b border-[var(--wraith-border)] shrink-0">
|
|
<div class="flex items-center gap-2 text-xs min-w-0">
|
|
<!-- File icon -->
|
|
<svg class="w-3.5 h-3.5 text-[var(--wraith-text-muted)] shrink-0" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.75 1.5a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6H9.75A1.75 1.75 0 0 1 8 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25V1.75z" />
|
|
</svg>
|
|
|
|
<span class="text-[var(--wraith-text-primary)] truncate font-mono text-[11px]">
|
|
{{ filePath }}
|
|
</span>
|
|
|
|
<!-- Unsaved indicator -->
|
|
<span v-if="hasUnsavedChanges" class="w-2 h-2 rounded-full bg-[var(--wraith-accent-yellow)] shrink-0" title="Unsaved changes" />
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<!-- Save button -->
|
|
<button
|
|
class="px-3 py-1 text-xs rounded transition-colors cursor-pointer"
|
|
:class="
|
|
hasUnsavedChanges
|
|
? 'bg-[var(--wraith-accent-blue)] text-white hover:bg-blue-600'
|
|
: 'bg-[var(--wraith-bg-tertiary)] text-[var(--wraith-text-muted)] cursor-not-allowed'
|
|
"
|
|
:disabled="!hasUnsavedChanges"
|
|
@click="handleSave"
|
|
>
|
|
Save
|
|
</button>
|
|
|
|
<!-- Close button -->
|
|
<button
|
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer"
|
|
@click="emit('close')"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CodeMirror container -->
|
|
<div ref="editorRef" class="flex-1 min-h-0 overflow-hidden" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightSpecialChars } from "@codemirror/view";
|
|
import { EditorState } from "@codemirror/state";
|
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language";
|
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
|
|
const props = defineProps<{
|
|
content: string;
|
|
filePath: string;
|
|
sessionId: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
close: [];
|
|
}>();
|
|
|
|
const editorRef = ref<HTMLElement | null>(null);
|
|
const hasUnsavedChanges = ref(false);
|
|
const editorHeight = ref(300);
|
|
|
|
let view: EditorView | null = null;
|
|
|
|
/** Detect language extension from file path. */
|
|
async function getLanguageExtension(path: string) {
|
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
switch (ext) {
|
|
case "js":
|
|
case "jsx":
|
|
case "ts":
|
|
case "tsx": {
|
|
const { javascript } = await import("@codemirror/lang-javascript");
|
|
return javascript({ jsx: ext.includes("x"), typescript: ext.startsWith("t") });
|
|
}
|
|
case "json": {
|
|
const { json } = await import("@codemirror/lang-json");
|
|
return json();
|
|
}
|
|
case "py": {
|
|
const { python } = await import("@codemirror/lang-python");
|
|
return python();
|
|
}
|
|
case "md":
|
|
case "markdown": {
|
|
const { markdown } = await import("@codemirror/lang-markdown");
|
|
return markdown();
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!editorRef.value) return;
|
|
|
|
const langExt = await getLanguageExtension(props.filePath);
|
|
|
|
const extensions = [
|
|
lineNumbers(),
|
|
highlightActiveLine(),
|
|
highlightSpecialChars(),
|
|
history(),
|
|
bracketMatching(),
|
|
closeBrackets(),
|
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
oneDark,
|
|
keymap.of([
|
|
...defaultKeymap,
|
|
...historyKeymap,
|
|
...closeBracketsKeymap,
|
|
]),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
hasUnsavedChanges.value = true;
|
|
}
|
|
}),
|
|
EditorView.theme({
|
|
"&": {
|
|
height: "100%",
|
|
fontSize: "13px",
|
|
},
|
|
".cm-scroller": {
|
|
overflow: "auto",
|
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
|
|
},
|
|
}),
|
|
];
|
|
|
|
if (langExt) {
|
|
extensions.push(langExt);
|
|
}
|
|
|
|
const state = EditorState.create({
|
|
doc: props.content,
|
|
extensions,
|
|
});
|
|
|
|
view = new EditorView({
|
|
state,
|
|
parent: editorRef.value,
|
|
});
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (view) {
|
|
view.destroy();
|
|
view = null;
|
|
}
|
|
});
|
|
|
|
// Re-create editor when content/filePath changes
|
|
watch(
|
|
() => props.filePath,
|
|
async () => {
|
|
if (!view || !editorRef.value) return;
|
|
hasUnsavedChanges.value = false;
|
|
|
|
const langExt = await getLanguageExtension(props.filePath);
|
|
const extensions = [
|
|
lineNumbers(),
|
|
highlightActiveLine(),
|
|
highlightSpecialChars(),
|
|
history(),
|
|
bracketMatching(),
|
|
closeBrackets(),
|
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
oneDark,
|
|
keymap.of([
|
|
...defaultKeymap,
|
|
...historyKeymap,
|
|
...closeBracketsKeymap,
|
|
]),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
hasUnsavedChanges.value = true;
|
|
}
|
|
}),
|
|
EditorView.theme({
|
|
"&": {
|
|
height: "100%",
|
|
fontSize: "13px",
|
|
},
|
|
".cm-scroller": {
|
|
overflow: "auto",
|
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
|
|
},
|
|
}),
|
|
];
|
|
|
|
if (langExt) {
|
|
extensions.push(langExt);
|
|
}
|
|
|
|
view.destroy();
|
|
|
|
const state = EditorState.create({
|
|
doc: props.content,
|
|
extensions,
|
|
});
|
|
|
|
view = new EditorView({
|
|
state,
|
|
parent: editorRef.value,
|
|
});
|
|
},
|
|
);
|
|
|
|
function handleSave(): void {
|
|
if (!view || !hasUnsavedChanges.value) return;
|
|
|
|
const _currentContent = view.state.doc.toString();
|
|
// TODO: Replace with Wails binding call — SFTPService.WriteFile(sessionId, filePath, currentContent)
|
|
void props.sessionId;
|
|
|
|
hasUnsavedChanges.value = false;
|
|
}
|
|
</script>
|