Move to threads based system.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { Button, Switch } from '@spoon/ui';
|
||||
|
||||
const MonacoEditor = dynamic(async () => await import('@monaco-editor/react'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
type MonacoEditorInstance = {
|
||||
getModel?: () => unknown;
|
||||
};
|
||||
|
||||
type VimMode = {
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export const CodeEditor = ({
|
||||
path,
|
||||
content,
|
||||
readOnly,
|
||||
onSave,
|
||||
}: {
|
||||
path?: string;
|
||||
content: string;
|
||||
readOnly: boolean;
|
||||
onSave: (content: string) => Promise<void>;
|
||||
}) => {
|
||||
const [value, setValue] = useState(content);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [vimEnabled, setVimEnabled] = useState(false);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null);
|
||||
const vimRef = useRef<VimMode | null>(null);
|
||||
const statusRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(content);
|
||||
setDirty(false);
|
||||
}, [content, path]);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
vimRef.current?.dispose();
|
||||
vimRef.current = null;
|
||||
if (!vimEnabled) return;
|
||||
void import('monaco-vim').then((module) => {
|
||||
const initVimMode = module.initVimMode as unknown as (
|
||||
editor: MonacoEditorInstance,
|
||||
statusNode?: HTMLElement | null,
|
||||
) => VimMode;
|
||||
vimRef.current = initVimMode(editor, statusRef.current);
|
||||
});
|
||||
return () => {
|
||||
vimRef.current?.dispose();
|
||||
vimRef.current = null;
|
||||
};
|
||||
}, [vimEnabled, path]);
|
||||
|
||||
if (!path) {
|
||||
return (
|
||||
<div className='text-muted-foreground flex h-full items-center justify-center text-sm'>
|
||||
Select a file to inspect or edit.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(value);
|
||||
setDirty(false);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex h-full min-h-[520px] flex-col'>
|
||||
<div className='border-border flex h-11 items-center justify-between gap-3 border-b px-3'>
|
||||
<div className='min-w-0'>
|
||||
<p className='truncate font-mono text-xs'>{path}</p>
|
||||
{dirty ? (
|
||||
<p className='text-muted-foreground text-xs'>Unsaved changes</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className='flex items-center gap-3'>
|
||||
<label className='flex items-center gap-2 text-xs'>
|
||||
Vim
|
||||
<Switch checked={vimEnabled} onCheckedChange={setVimEnabled} />
|
||||
</label>
|
||||
<Button
|
||||
type='button'
|
||||
size='sm'
|
||||
disabled={readOnly || saving || !dirty}
|
||||
onClick={save}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1'>
|
||||
<MonacoEditor
|
||||
height='520px'
|
||||
path={path}
|
||||
value={value}
|
||||
theme='vs-dark'
|
||||
options={{
|
||||
readOnly,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor as MonacoEditorInstance;
|
||||
}}
|
||||
onChange={(next) => {
|
||||
setValue(next ?? '');
|
||||
setDirty((next ?? '') !== content);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={statusRef}
|
||||
className='border-border text-muted-foreground h-6 border-t px-3 py-1 font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user