feat: CodeMirror 6 inline editor with dark theme and language detection
Add EditorWindow component with CodeMirror 6 using one-dark theme. Detects language from file extension (js/ts/jsx/tsx, json, py, md) via dynamic imports for code splitting. Features unsaved changes indicator (yellow dot), Save button (TODO: SFTPService.WriteFile), and Close button. Renders as an inline panel above the terminal. File clicks in FileTree open the editor with mock content. Editor re-creates when file path changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3898a1c3e2
commit
325cebbd01
229
frontend/src/components/editor/EditorWindow.vue
Normal file
229
frontend/src/components/editor/EditorWindow.vue
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
<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>
|
||||||
Loading…
Reference in New Issue
Block a user