Code editor and widget
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import Editor, { type OnMount } from '@monaco-editor/react';
|
||||
import type { editor as MonacoEditor } from 'monaco-editor';
|
||||
import { toast } from 'sonner';
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
|
||||
import { useFiles } from 'widgets/FileBrowser';
|
||||
import { FileTree } from './FileTree';
|
||||
import { EditorTabs } from './EditorTabs';
|
||||
import { useEditorState } from './useEditorState';
|
||||
import { getLanguage } from './language-map';
|
||||
|
||||
type CodeEditorViewProps = {
|
||||
className?: string;
|
||||
theme?: string;
|
||||
root?: string;
|
||||
initialPath?: string;
|
||||
};
|
||||
|
||||
export const CodeEditorView = ({ className, theme = 'vs-dark', root = 'home', initialPath }: CodeEditorViewProps) => {
|
||||
const { files, activePath, setActivePath, openFile, closeFile, setContent, markSaved, getActiveFile } =
|
||||
useEditorState();
|
||||
const { readFile, writeFile } = useFiles(root);
|
||||
const editorRef = useRef<MonacoEditor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
const activeFile = getActiveFile();
|
||||
|
||||
const handleOpenFile = useCallback(
|
||||
async (path: string, name: string) => {
|
||||
const existing = files.find((f) => f.path === path);
|
||||
if (existing) {
|
||||
setActivePath(path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await readFile(path);
|
||||
openFile(path, name, res.content);
|
||||
} catch {
|
||||
toast.error('Failed to read file');
|
||||
}
|
||||
},
|
||||
[files, setActivePath, readFile, openFile],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!activeFile || !activeFile.isDirty) return;
|
||||
try {
|
||||
await writeFile(activeFile.path, activeFile.content);
|
||||
markSaved(activeFile.path, activeFile.content);
|
||||
toast.success('File saved');
|
||||
} catch {
|
||||
toast.error('Failed to save file');
|
||||
}
|
||||
}, [activeFile, writeFile, markSaved]);
|
||||
|
||||
const handleEditorMount: OnMount = useCallback(
|
||||
(editor, monaco) => {
|
||||
editorRef.current = editor;
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||
handleSave();
|
||||
});
|
||||
},
|
||||
[handleSave],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (ev: KeyboardEvent) => {
|
||||
if ((ev.ctrlKey || ev.metaKey) && ev.key === 's') {
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, []);
|
||||
|
||||
const bg = theme === 'vs-dark' ? '#1e1e1e' : theme === 'vs' ? '#ffffff' : '#1e1e1e';
|
||||
const borderColor = theme === 'vs-dark' ? '#333' : '#e0e0e0';
|
||||
|
||||
return (
|
||||
<div className={className} style={{ background: bg }}>
|
||||
<ResizablePanelGroup direction="horizontal" className="h-full rounded-lg" style={{ borderColor }}>
|
||||
<ResizablePanel defaultSize={20} minSize={10} maxSize={40}>
|
||||
<div className="h-full flex flex-col" style={{ background: bg }}>
|
||||
<div
|
||||
className="px-3 py-2 text-xs font-semibold uppercase tracking-wider"
|
||||
style={{ color: '#888', borderBottom: `1px solid ${borderColor}` }}
|
||||
>
|
||||
Explorer
|
||||
</div>
|
||||
<FileTree root={root} basePath={initialPath ?? '/'} onOpenFile={handleOpenFile} />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={80}>
|
||||
<div className="h-full flex flex-col" style={{ background: bg }}>
|
||||
<EditorTabs
|
||||
files={files}
|
||||
activePath={activePath}
|
||||
onSelect={setActivePath}
|
||||
onClose={closeFile}
|
||||
theme={theme}
|
||||
/>
|
||||
{activeFile ? (
|
||||
<Editor
|
||||
key={activeFile.path}
|
||||
theme={theme}
|
||||
language={getLanguage(activeFile.name)}
|
||||
value={activeFile.content}
|
||||
onChange={(value) => setContent(activeFile.path, value ?? '')}
|
||||
onMount={handleEditorMount}
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
tabSize: 2,
|
||||
wordWrap: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
renderWhitespace: 'selection',
|
||||
smoothScrolling: true,
|
||||
cursorBlinking: 'smooth',
|
||||
cursorSmoothCaretAnimation: 'on',
|
||||
padding: { top: 8 },
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-sm" style={{ color: '#888' }}>
|
||||
Open a file from the explorer to start editing
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import type { OpenFile } from './useEditorState';
|
||||
|
||||
type EditorTabsProps = {
|
||||
files: OpenFile[];
|
||||
activePath: string | null;
|
||||
onSelect: (path: string) => void;
|
||||
onClose: (path: string) => void;
|
||||
theme?: string;
|
||||
};
|
||||
|
||||
const FileIcon = ({ name }: { name: string }) => {
|
||||
const svg = useMemo(() => getIcon(name).svg, [name]);
|
||||
return <span className="inline-flex h-4 w-4 shrink-0" dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||
};
|
||||
|
||||
export const EditorTabs = ({ files, activePath, onSelect, onClose, theme }: EditorTabsProps) => {
|
||||
if (files.length === 0) return null;
|
||||
|
||||
const isDark = theme !== 'vs';
|
||||
const borderColor = isDark ? '#333' : '#e0e0e0';
|
||||
const activeBg = isDark ? '#1e1e1e' : '#ffffff';
|
||||
const inactiveBg = isDark ? '#181818' : '#f3f3f3';
|
||||
const activeColor = isDark ? '#ccc' : '#333';
|
||||
const inactiveColor = isDark ? '#888' : '#666';
|
||||
|
||||
return (
|
||||
<div className="flex items-center overflow-x-auto shrink-0 code-editor-scrollable" style={{ borderBottom: `1px solid ${borderColor}` }}>
|
||||
{files.map((file) => {
|
||||
const isActive = file.path === activePath;
|
||||
return (
|
||||
<button
|
||||
key={file.path}
|
||||
onClick={() => onSelect(file.path)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm cursor-pointer whitespace-nowrap transition-colors"
|
||||
style={{
|
||||
background: isActive ? activeBg : inactiveBg,
|
||||
color: isActive ? activeColor : inactiveColor,
|
||||
borderRight: `1px solid ${borderColor}`,
|
||||
}}
|
||||
>
|
||||
<FileIcon name={file.name} />
|
||||
<span>{file.name}</span>
|
||||
{file.isDirty && <span className="w-2 h-2 rounded-full bg-blue-400 shrink-0" />}
|
||||
<span
|
||||
role="button"
|
||||
className="ml-1 p-0.5 rounded transition-colors"
|
||||
style={{ color: inactiveColor }}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onClose(file.path);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { ChevronRight, ChevronDown, Folder } from 'lucide-react';
|
||||
import { getIcon } from 'material-file-icons';
|
||||
import { useFiles, type DirEntry } from 'widgets/FileBrowser';
|
||||
|
||||
type FileTreeProps = {
|
||||
root: string;
|
||||
basePath: string;
|
||||
onOpenFile: (path: string, name: string) => void;
|
||||
};
|
||||
|
||||
type TreeNodeProps = {
|
||||
entry: DirEntry;
|
||||
parentPath: string;
|
||||
root: string;
|
||||
onOpenFile: (path: string, name: string) => void;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
const FileIcon = ({ name }: { name: string }) => {
|
||||
const svg = useMemo(() => getIcon(name).svg, [name]);
|
||||
return <span className="inline-flex h-4 w-4 shrink-0" dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||
};
|
||||
|
||||
const sortEntries = (entries: DirEntry[]) => {
|
||||
const dirs = entries.filter((e) => e.type === 'directory').sort((a, b) => a.name.localeCompare(b.name));
|
||||
const files = entries.filter((e) => e.type === 'file').sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [...dirs, ...files];
|
||||
};
|
||||
|
||||
const TreeNode = ({ entry, parentPath, root, onOpenFile, depth }: TreeNodeProps) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [children, setChildren] = useState<DirEntry[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { listDir } = useFiles(root);
|
||||
const isDir = entry.type === 'directory';
|
||||
const fullPath = parentPath === '/' ? `/${entry.name}` : `${parentPath}/${entry.name}`;
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
if (!isDir) {
|
||||
onOpenFile(fullPath, entry.name);
|
||||
return;
|
||||
}
|
||||
if (!expanded && children === null) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDir(fullPath);
|
||||
setChildren(sortEntries(res.entries));
|
||||
} catch {
|
||||
setChildren([]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
setExpanded((prev) => !prev);
|
||||
}, [isDir, expanded, children, fullPath, entry.name, listDir, onOpenFile]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center gap-1 w-full px-1 py-0.5 text-sm rounded cursor-pointer transition-colors text-left text-[#ccc] hover:bg-white/5"
|
||||
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
||||
>
|
||||
{isDir ? (
|
||||
<>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-[#888]" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-[#888]" />
|
||||
)}
|
||||
<Folder className="h-4 w-4 shrink-0 text-duck-yellow fill-duck-yellow/30" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="w-3.5 shrink-0" />
|
||||
<FileIcon name={entry.name} />
|
||||
</>
|
||||
)}
|
||||
<span className="truncate">{entry.name}</span>
|
||||
{loading && <span className="text-xs text-[#888] ml-auto">...</span>}
|
||||
</button>
|
||||
{isDir && expanded && children && (
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<TreeNode
|
||||
key={child.name}
|
||||
entry={child}
|
||||
parentPath={fullPath}
|
||||
root={root}
|
||||
onOpenFile={onOpenFile}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileTree = ({ root, basePath, onOpenFile }: FileTreeProps) => {
|
||||
const { listDir } = useFiles(root);
|
||||
const [entries, setEntries] = useState<DirEntry[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadRoot = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDir(basePath);
|
||||
setEntries(sortEntries(res.entries));
|
||||
} catch {
|
||||
setEntries([]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [listDir, basePath]);
|
||||
|
||||
if (entries === null && !loading) {
|
||||
loadRoot();
|
||||
}
|
||||
|
||||
if (loading && entries === null) {
|
||||
return <div className="p-2 text-sm text-[#888]">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!entries || entries.length === 0) {
|
||||
return <div className="p-2 text-sm text-[#888]">No files</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-1 overflow-y-auto h-full code-editor-scrollable">
|
||||
{entries.map((entry) => (
|
||||
<TreeNode
|
||||
key={entry.name}
|
||||
entry={entry}
|
||||
parentPath={basePath}
|
||||
root={root}
|
||||
onOpenFile={onOpenFile}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CodeEditorView } from './CodeEditor';
|
||||
@@ -0,0 +1,235 @@
|
||||
const extensionToLanguage: Record<string, string> = {
|
||||
// JavaScript / TypeScript
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
|
||||
// Web
|
||||
html: 'html',
|
||||
htm: 'html',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
less: 'less',
|
||||
vue: 'html',
|
||||
svelte: 'html',
|
||||
|
||||
// Data / Config
|
||||
json: 'json',
|
||||
jsonc: 'json',
|
||||
json5: 'json',
|
||||
geojson: 'json',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
toml: 'ini',
|
||||
ini: 'ini',
|
||||
cfg: 'ini',
|
||||
conf: 'ini',
|
||||
properties: 'ini',
|
||||
env: 'ini',
|
||||
xml: 'xml',
|
||||
xsl: 'xml',
|
||||
xslt: 'xml',
|
||||
xsd: 'xml',
|
||||
svg: 'xml',
|
||||
plist: 'xml',
|
||||
csproj: 'xml',
|
||||
fsproj: 'xml',
|
||||
vcxproj: 'xml',
|
||||
sln: 'xml',
|
||||
|
||||
// Markdown / Text
|
||||
md: 'markdown',
|
||||
mdx: 'markdown',
|
||||
markdown: 'markdown',
|
||||
txt: 'plaintext',
|
||||
log: 'plaintext',
|
||||
|
||||
// Shell
|
||||
sh: 'shell',
|
||||
bash: 'shell',
|
||||
zsh: 'shell',
|
||||
fish: 'shell',
|
||||
ksh: 'shell',
|
||||
csh: 'shell',
|
||||
ps1: 'powershell',
|
||||
psm1: 'powershell',
|
||||
psd1: 'powershell',
|
||||
bat: 'bat',
|
||||
cmd: 'bat',
|
||||
|
||||
// Python
|
||||
py: 'python',
|
||||
pyw: 'python',
|
||||
pyi: 'python',
|
||||
pyx: 'python',
|
||||
ipynb: 'json',
|
||||
|
||||
// Ruby
|
||||
rb: 'ruby',
|
||||
erb: 'ruby',
|
||||
gemspec: 'ruby',
|
||||
rake: 'ruby',
|
||||
|
||||
// Rust
|
||||
rs: 'rust',
|
||||
|
||||
// Go
|
||||
go: 'go',
|
||||
mod: 'go',
|
||||
|
||||
// Java / JVM
|
||||
java: 'java',
|
||||
kt: 'kotlin',
|
||||
kts: 'kotlin',
|
||||
scala: 'scala',
|
||||
sc: 'scala',
|
||||
groovy: 'groovy',
|
||||
gradle: 'groovy',
|
||||
|
||||
// C / C++ / Objective-C
|
||||
c: 'c',
|
||||
h: 'c',
|
||||
cpp: 'cpp',
|
||||
cc: 'cpp',
|
||||
cxx: 'cpp',
|
||||
hpp: 'cpp',
|
||||
hxx: 'cpp',
|
||||
hh: 'cpp',
|
||||
m: 'objective-c',
|
||||
mm: 'objective-c',
|
||||
|
||||
// C# / F#
|
||||
cs: 'csharp',
|
||||
csx: 'csharp',
|
||||
fs: 'fsharp',
|
||||
fsx: 'fsharp',
|
||||
fsi: 'fsharp',
|
||||
|
||||
// Swift
|
||||
swift: 'swift',
|
||||
|
||||
// Dart
|
||||
dart: 'dart',
|
||||
|
||||
// PHP
|
||||
php: 'php',
|
||||
phtml: 'php',
|
||||
|
||||
// SQL
|
||||
sql: 'sql',
|
||||
mysql: 'sql',
|
||||
pgsql: 'pgsql',
|
||||
|
||||
// Lua
|
||||
lua: 'lua',
|
||||
|
||||
// R
|
||||
r: 'r',
|
||||
rmd: 'markdown',
|
||||
|
||||
// Perl
|
||||
pl: 'perl',
|
||||
pm: 'perl',
|
||||
perl: 'perl',
|
||||
|
||||
// GraphQL
|
||||
graphql: 'graphql',
|
||||
gql: 'graphql',
|
||||
|
||||
// Docker
|
||||
dockerfile: 'dockerfile',
|
||||
|
||||
// Elixir / Erlang
|
||||
ex: 'elixir',
|
||||
exs: 'elixir',
|
||||
erl: 'erlang',
|
||||
hrl: 'erlang',
|
||||
|
||||
// Haskell
|
||||
hs: 'haskell',
|
||||
lhs: 'haskell',
|
||||
|
||||
// Clojure
|
||||
clj: 'clojure',
|
||||
cljs: 'clojure',
|
||||
cljc: 'clojure',
|
||||
edn: 'clojure',
|
||||
|
||||
// Handlebars
|
||||
hbs: 'handlebars',
|
||||
handlebars: 'handlebars',
|
||||
|
||||
// Twig
|
||||
twig: 'twig',
|
||||
|
||||
// Pug
|
||||
pug: 'pug',
|
||||
jade: 'pug',
|
||||
|
||||
// Coffee
|
||||
coffee: 'coffeescript',
|
||||
|
||||
// Diff / Patch
|
||||
diff: 'diff',
|
||||
patch: 'diff',
|
||||
|
||||
// Protocol Buffers
|
||||
proto: 'protobuf',
|
||||
|
||||
// Terraform
|
||||
tf: 'hcl',
|
||||
tfvars: 'hcl',
|
||||
hcl: 'hcl',
|
||||
|
||||
// ABAP
|
||||
abap: 'abap',
|
||||
|
||||
// Apex
|
||||
apex: 'apex',
|
||||
cls: 'apex',
|
||||
trigger: 'apex',
|
||||
|
||||
// Pascal
|
||||
pas: 'pascal',
|
||||
pp: 'pascal',
|
||||
|
||||
// Tcl
|
||||
tcl: 'tcl',
|
||||
|
||||
// Scheme / Lisp
|
||||
scm: 'scheme',
|
||||
ss: 'scheme',
|
||||
rkt: 'scheme',
|
||||
lisp: 'scheme',
|
||||
lsp: 'scheme',
|
||||
el: 'scheme',
|
||||
|
||||
// Misc
|
||||
sol: 'sol',
|
||||
bicep: 'bicep',
|
||||
azcli: 'azcli',
|
||||
redis: 'redis',
|
||||
sb: 'sb',
|
||||
st: 'st',
|
||||
lex: 'lexon',
|
||||
};
|
||||
|
||||
export const getLanguage = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
||||
const base = filename.toLowerCase();
|
||||
if (base === 'dockerfile' || base.startsWith('dockerfile.')) return 'dockerfile';
|
||||
if (base === 'makefile' || base === 'gnumakefile') return 'shell';
|
||||
if (base === 'gemfile' || base === 'rakefile' || base === 'vagrantfile') return 'ruby';
|
||||
if (base === 'justfile') return 'shell';
|
||||
if (base === '.gitignore' || base === '.dockerignore' || base === '.editorconfig') return 'ini';
|
||||
if (base === '.prettierrc' || base === '.eslintrc' || base === 'tsconfig.json' || base === 'package.json')
|
||||
return 'json';
|
||||
// Dotfiles that are shell scripts
|
||||
if (/^\.(bash|zsh|sh|ksh|csh)rc$/.test(base)) return 'shell';
|
||||
if (/^\.(bash_|zsh_|sh_)/.test(base)) return 'shell'; // .bash_profile, .zsh_history, etc.
|
||||
if (base === '.profile' || base === '.bash_logout' || base === '.bash_login') return 'shell';
|
||||
return extensionToLanguage[ext] ?? 'plaintext';
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export type OpenFile = {
|
||||
path: string;
|
||||
name: string;
|
||||
content: string;
|
||||
originalContent: string;
|
||||
isDirty: boolean;
|
||||
};
|
||||
|
||||
export const useEditorState = () => {
|
||||
const [files, setFiles] = useState<OpenFile[]>([]);
|
||||
const [activePath, setActivePath] = useState<string | null>(null);
|
||||
|
||||
const openFile = useCallback((path: string, name: string, content: string) => {
|
||||
setFiles((prev) => {
|
||||
const existing = prev.find((f) => f.path === path);
|
||||
if (existing) return prev;
|
||||
return [...prev, { path, name, content, originalContent: content, isDirty: false }];
|
||||
});
|
||||
setActivePath(path);
|
||||
}, []);
|
||||
|
||||
const closeFile = useCallback(
|
||||
(path: string) => {
|
||||
setFiles((prev) => {
|
||||
const next = prev.filter((f) => f.path !== path);
|
||||
if (activePath === path) {
|
||||
const idx = prev.findIndex((f) => f.path === path);
|
||||
const newActive = next[Math.min(idx, next.length - 1)] ?? null;
|
||||
setActivePath(newActive?.path ?? null);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[activePath],
|
||||
);
|
||||
|
||||
const setContent = useCallback((path: string, content: string) => {
|
||||
setFiles((prev) =>
|
||||
prev.map((f) => (f.path === path ? { ...f, content, isDirty: content !== f.originalContent } : f)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const markSaved = useCallback((path: string, content: string) => {
|
||||
setFiles((prev) =>
|
||||
prev.map((f) => (f.path === path ? { ...f, originalContent: content, content, isDirty: false } : f)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getActiveFile = useCallback((): OpenFile | null => {
|
||||
return files.find((f) => f.path === activePath) ?? null;
|
||||
}, [files, activePath]);
|
||||
|
||||
return { files, activePath, setActivePath, openFile, closeFile, setContent, markSaved, getActiveFile };
|
||||
};
|
||||
@@ -34,6 +34,9 @@ export const useFiles = (root: string = 'home') => {
|
||||
readFile: (path: string) =>
|
||||
client.get<{ content: string; size: number }>(withRoot(`/file-browser/read?path=${encodeURIComponent(path)}`)),
|
||||
|
||||
writeFile: (path: string, content: string) =>
|
||||
client.post(withRoot('/file-browser/write'), { path, content }),
|
||||
|
||||
search: (query: string) =>
|
||||
client.get<{ results: DirEntry[] }>(withRoot(`/file-browser/search?q=${encodeURIComponent(query)}`)),
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"./Terminal": "./Terminal/index.ts",
|
||||
"./FileBrowser": "./FileBrowser/index.ts",
|
||||
"./ChatHistory": "./ChatHistory/index.ts",
|
||||
"./Chat": "./Chat/index.ts"
|
||||
"./Chat": "./Chat/index.ts",
|
||||
"./CodeEditor": "./CodeEditor/index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user