Files
kandev/apps/web/components/task/file-editor-content.tsx
T
2026-02-02 22:45:35 +00:00

151 lines
4.2 KiB
TypeScript

'use client';
import { useCallback, useEffect, useMemo } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
import { EditorView } from '@codemirror/view';
import type { Extension } from '@codemirror/state';
import { getCodeMirrorExtensionFromPath } from '@/lib/languages';
import { Button } from '@kandev/ui/button';
import { IconDeviceFloppy, IconLoader2 } from '@tabler/icons-react';
import { formatDiffStats } from '@/lib/utils/file-diff';
type FileEditorContentProps = {
path: string;
content: string;
originalContent: string;
isDirty: boolean;
isSaving: boolean;
onChange: (newContent: string) => void;
onSave: () => void;
};
export function FileEditorContent({
path,
content,
originalContent,
isDirty,
isSaving,
onChange,
onSave,
}: FileEditorContentProps) {
const langExt = getCodeMirrorExtensionFromPath(path);
const extensions: Extension[] = [
EditorView.lineWrapping,
EditorView.editable.of(true),
];
if (langExt) {
extensions.push(langExt);
}
// Calculate diff stats when dirty
const diffStats = useMemo(() => {
if (!isDirty) return null;
// Simple line-based diff for stats
const originalLines = originalContent.split('\n');
const currentLines = content.split('\n');
let additions = 0;
let deletions = 0;
// Simple comparison - count different lines
const maxLen = Math.max(originalLines.length, currentLines.length);
for (let i = 0; i < maxLen; i++) {
const origLine = originalLines[i];
const currLine = currentLines[i];
if (origLine === undefined && currLine !== undefined) {
additions++;
} else if (origLine !== undefined && currLine === undefined) {
deletions++;
} else if (origLine !== currLine) {
// Line changed - count as both addition and deletion
additions++;
deletions++;
}
}
return { additions, deletions };
}, [isDirty, content, originalContent]);
// Handle Cmd/Ctrl+S keyboard shortcut
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
if (isDirty && !isSaving) {
onSave();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isDirty, isSaving, onSave]);
const handleChange = useCallback(
(value: string) => {
onChange(value);
},
[onChange]
);
return (
<div className="flex h-full flex-col">
{/* Editor header with save button */}
<div className="flex items-center justify-between border-b border-border bg-muted/30 px-4 py-2">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-mono">{path}</span>
{isDirty && diffStats && (
<span className="text-xs text-yellow-500">
{formatDiffStats(diffStats.additions, diffStats.deletions)}
</span>
)}
</div>
<Button
size="sm"
variant="default"
onClick={onSave}
disabled={!isDirty || isSaving}
className="cursor-pointer gap-2"
>
{isSaving ? (
<>
<IconLoader2 className="h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<IconDeviceFloppy className="h-4 w-4" />
Save
<span className="text-xs text-muted-foreground">
({navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+S)
</span>
</>
)}
</Button>
</div>
{/* CodeMirror editor */}
<div className="flex-1 overflow-hidden">
<CodeMirror
value={content}
height="100%"
theme={vscodeDark}
extensions={extensions}
onChange={handleChange}
basicSetup={{
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
highlightSelectionMatches: true,
}}
className="h-full overflow-auto text-sm"
/>
</div>
</div>
);
}