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