feat: use CodeMirror for always-editable code file preview

- Add FileCodeEditor.svelte: CodeMirror wrapper with auto language
  detection, dark mode, Ctrl+S save, reactive to value/filePath changes
- Replace Shiki read-only highlighting + textarea editing with
  always-editable CodeMirror for code files in FileNav preview
- Show persistent Save button for code files in toolbar
- Non-code text files keep existing Edit/Save/Cancel textarea flow
- SVG retains Shiki highlighting for visual preview mode
This commit is contained in:
Timothy Jaeryang Baek committed 2026-03-06 15:03:23 -06:00
1 parent 39deadcab1
commit 305e591ec2
3 files changed
+191 -18

No files matched your search

@@ -4,15 +4,17 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { settings } from '$lib/stores';
import { isCodeFile, highlightCode } from '$lib/utils/codeHighlight';
import { isCodeFile } from '$lib/utils/codeHighlight';
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
import Spinner from '../../common/Spinner.svelte';
import PDFViewer from '../../common/PDFViewer.svelte';
import JsonTreeView from './JsonTreeView.svelte';
import NotebookView from './NotebookView.svelte';
import SqliteView from './SqliteView.svelte';
import FileCodeEditor from './FileCodeEditor.svelte';
let pdfViewerRef: PDFViewer;
let fileCodeEditorRef: FileCodeEditor;
const i18n = getContext('i18n');
@@ -76,6 +78,15 @@
editContent = '';
};
/** Save code file directly from CodeMirror */
export const saveCodeFile = async () => {
if (!onSave) return;
saving = true;
const content = fileCodeEditorRef?.getValue() ?? '';
await onSave(content);
saving = false;
};
$: isTextFile = fileContent !== null && fileImageUrl === null && filePdfData === null;
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
@@ -175,24 +186,21 @@
$: csvHeader = csvRows.length > 0 ? csvRows[0] : [];
$: csvBody = csvRows.length > 1 ? csvRows.slice(1) : [];
// ── Shiki code highlighting ─────────────────────────────────────────
// ── Shiki code highlighting (SVG only) ──────────────────────────────
let highlightedHtml: string | null = null;
let highlightingFile: string | null = null; // track which file we're highlighting
let highlightingFile: string | null = null;
$: if ((isCode || isSvg) && fileContent !== null && selectedFile) {
$: if (isSvg && fileContent !== null && selectedFile) {
const currentFile = selectedFile;
highlightingFile = currentFile;
const lang = isSvg ? 'xml' : undefined;
(lang
? import('shiki').then(({ codeToHtml }) =>
codeToHtml(fileContent!, {
lang: 'xml',
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: 'light'
})
)
: highlightCode(fileContent!, selectedFile!)
)
import('shiki')
.then(({ codeToHtml }) =>
codeToHtml(fileContent!, {
lang: 'xml',
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: 'light'
})
)
.then((html) => {
if (highlightingFile === currentFile) highlightedHtml = html;
})
@@ -428,7 +436,16 @@
<div class="svg-preview w-full h-full flex items-center justify-center overflow-auto p-3">
{@html DOMPurify.sanitize(fileContent, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ['use'] })}
</div>
{:else if (isCode || isSvg) && highlightedHtml && !showRaw}
{:else if isCode && !showRaw}
<div class="h-full">
<FileCodeEditor
bind:this={fileCodeEditorRef}
value={fileContent ?? ''}
filePath={selectedFile}
{onSave}
/>
</div>
{:else if isSvg && highlightedHtml && !showRaw}
<div class="shiki-preview overflow-auto h-full text-xs">
{@html highlightedHtml}
</div>