generated from nhcarrigan/template
feat: add built-in file editor with syntax highlighting #79
@@ -463,6 +463,78 @@ pub async fn write_file_content(path: String, content: String) -> Result<(), Str
|
||||
.map_err(|e| format!("Failed to write file: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_file(path: String) -> Result<(), String> {
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
if file_path.exists() {
|
||||
return Err("File already exists".to_string());
|
||||
}
|
||||
|
||||
File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_directory(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if dir_path.exists() {
|
||||
return Err("Directory already exists".to_string());
|
||||
}
|
||||
|
||||
fs::create_dir_all(dir_path).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_file(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(&path);
|
||||
|
||||
if !file_path.exists() {
|
||||
return Err("File does not exist".to_string());
|
||||
}
|
||||
|
||||
if file_path.is_dir() {
|
||||
return Err("Path is a directory, use delete_directory instead".to_string());
|
||||
}
|
||||
|
||||
fs::remove_file(file_path).map_err(|e| format!("Failed to delete file: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_directory(path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let dir_path = Path::new(&path);
|
||||
|
||||
if !dir_path.exists() {
|
||||
return Err("Directory does not exist".to_string());
|
||||
}
|
||||
|
||||
if !dir_path.is_dir() {
|
||||
return Err("Path is not a directory".to_string());
|
||||
}
|
||||
|
||||
fs::remove_dir_all(dir_path).map_err(|e| format!("Failed to delete directory: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -154,6 +154,10 @@ pub fn run() {
|
||||
list_directory,
|
||||
read_file_content,
|
||||
write_file_content,
|
||||
create_file,
|
||||
create_directory,
|
||||
delete_file,
|
||||
delete_directory,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
{ keys: ["Ctrl", "B"], description: "Toggle file browser" },
|
||||
{ keys: ["Ctrl", "S"], description: "Save current file" },
|
||||
{ keys: ["Ctrl", "W"], description: "Close current tab" },
|
||||
{ keys: ["Ctrl", "N"], description: "New file" },
|
||||
{ keys: ["Right-click"], description: "Context menu (New/Delete)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
destructive?: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
message,
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
destructive = false,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: Props = $props();
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onCancel();
|
||||
} else if (event.key === "Enter") {
|
||||
onConfirm();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onclick={onCancel}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-md rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 class="mb-2 text-lg font-semibold text-gray-100">{title}</h2>
|
||||
<p class="mb-6 text-sm text-gray-300">{message}</p>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
class="rounded-md border border-gray-600 bg-gray-700 px-4 py-2 text-sm text-gray-200 hover:bg-gray-600"
|
||||
onclick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md px-4 py-2 text-sm text-white {destructive
|
||||
? 'bg-red-600 hover:bg-red-700'
|
||||
: 'bg-pink-600 hover:bg-pink-700'}"
|
||||
onclick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,36 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { editorStore } from "$lib/stores/editor";
|
||||
import FileTreeItem from "./FileTreeItem.svelte";
|
||||
import FileContextMenu from "./FileContextMenu.svelte";
|
||||
import InputDialog from "./InputDialog.svelte";
|
||||
import ConfirmDialog from "./ConfirmDialog.svelte";
|
||||
import type { FileEntry } from "$lib/types/editor";
|
||||
|
||||
const fileTree = editorStore.fileTree;
|
||||
const isLoadingTree = editorStore.isLoadingTree;
|
||||
const currentDirectory = editorStore.currentDirectory;
|
||||
|
||||
// Listen for Ctrl+N keyboard shortcut from +page.svelte
|
||||
function handleNewFileEvent() {
|
||||
handleNewFile();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("editor-new-file", handleNewFileEvent);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("editor-new-file", handleNewFileEvent);
|
||||
});
|
||||
|
||||
// Context menu state
|
||||
let contextMenu = $state<{
|
||||
show: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
targetEntry: FileEntry | null;
|
||||
}>({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetEntry: null,
|
||||
});
|
||||
|
||||
// Dialog state
|
||||
let dialog = $state<{
|
||||
type: "newFile" | "newFolder" | "delete" | null;
|
||||
parentPath: string;
|
||||
targetEntry: FileEntry | null;
|
||||
}>({
|
||||
type: null,
|
||||
parentPath: "",
|
||||
targetEntry: null,
|
||||
});
|
||||
|
||||
function handleRefresh() {
|
||||
const dir = $currentDirectory;
|
||||
if (dir) {
|
||||
editorStore.initializeFileTree(dir);
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextMenu(event: MouseEvent, entry: FileEntry | null = null) {
|
||||
event.preventDefault();
|
||||
contextMenu = {
|
||||
show: true,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
targetEntry: entry,
|
||||
};
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenu = { show: false, x: 0, y: 0, targetEntry: null };
|
||||
}
|
||||
|
||||
function openNewFileDialog(parentPath: string) {
|
||||
dialog = { type: "newFile", parentPath, targetEntry: null };
|
||||
}
|
||||
|
||||
function openNewFolderDialog(parentPath: string) {
|
||||
dialog = { type: "newFolder", parentPath, targetEntry: null };
|
||||
}
|
||||
|
||||
function openDeleteDialog(entry: FileEntry) {
|
||||
dialog = { type: "delete", parentPath: "", targetEntry: entry };
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
dialog = { type: null, parentPath: "", targetEntry: null };
|
||||
}
|
||||
|
||||
async function handleCreateFile(fileName: string) {
|
||||
await editorStore.createFile(dialog.parentPath, fileName);
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
async function handleCreateFolder(folderName: string) {
|
||||
await editorStore.createDirectory(dialog.parentPath, folderName);
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!dialog.targetEntry) return;
|
||||
|
||||
if (dialog.targetEntry.isDirectory) {
|
||||
await editorStore.deleteDirectory(dialog.targetEntry.path);
|
||||
} else {
|
||||
await editorStore.deleteFile(dialog.targetEntry.path);
|
||||
}
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
openNewFileDialog($currentDirectory);
|
||||
}
|
||||
|
||||
function handleNewFolder() {
|
||||
openNewFolderDialog($currentDirectory);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="file-browser">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="file-browser" oncontextmenu={(e) => handleContextMenu(e, null)}>
|
||||
<div class="header">
|
||||
<span class="title">Files</span>
|
||||
<button class="refresh-button" on:click={handleRefresh} title="Refresh file tree">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M23 4v6h-6" />
|
||||
<path d="M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-buttons">
|
||||
<button class="header-button" onclick={handleNewFile} title="New File (Ctrl+N)">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<path d="M14 2v6h6" />
|
||||
<path d="M12 18v-6" />
|
||||
<path d="M9 15h6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="header-button" onclick={handleNewFolder} title="New Folder">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
||||
<path d="M12 11v6" />
|
||||
<path d="M9 14h6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="header-button" onclick={handleRefresh} title="Refresh file tree">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M23 4v6h-6" />
|
||||
<path d="M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tree-container">
|
||||
@@ -63,13 +196,60 @@
|
||||
{:else}
|
||||
<div class="tree">
|
||||
{#each $fileTree as entry (entry.path)}
|
||||
<FileTreeItem {entry} />
|
||||
<FileTreeItem {entry} onContextMenu={handleContextMenu} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if contextMenu.show}
|
||||
<FileContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
targetEntry={contextMenu.targetEntry}
|
||||
currentDirectory={$currentDirectory}
|
||||
onNewFile={openNewFileDialog}
|
||||
onNewFolder={openNewFolderDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
onClose={closeContextMenu}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if dialog.type === "newFile"}
|
||||
<InputDialog
|
||||
title="New File"
|
||||
placeholder="Enter file name..."
|
||||
confirmText="Create"
|
||||
onConfirm={handleCreateFile}
|
||||
onCancel={closeDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if dialog.type === "newFolder"}
|
||||
<InputDialog
|
||||
title="New Folder"
|
||||
placeholder="Enter folder name..."
|
||||
confirmText="Create"
|
||||
onConfirm={handleCreateFolder}
|
||||
onCancel={closeDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if dialog.type === "delete" && dialog.targetEntry}
|
||||
<ConfirmDialog
|
||||
title="Delete {dialog.targetEntry.isDirectory ? 'Folder' : 'File'}"
|
||||
message="Are you sure you want to delete '{dialog.targetEntry.name}'? {dialog.targetEntry
|
||||
.isDirectory
|
||||
? 'This will also delete all files and folders inside it.'
|
||||
: 'This action cannot be undone.'}"
|
||||
confirmText="Delete"
|
||||
destructive={true}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={closeDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.file-browser {
|
||||
display: flex;
|
||||
@@ -96,7 +276,12 @@
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -109,7 +294,7 @@
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
.header-button:hover {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import type { FileEntry } from "$lib/types/editor";
|
||||
|
||||
interface Props {
|
||||
x: number;
|
||||
y: number;
|
||||
targetEntry: FileEntry | null;
|
||||
currentDirectory: string;
|
||||
onNewFile: (parentPath: string) => void;
|
||||
onNewFolder: (parentPath: string) => void;
|
||||
onDelete: (entry: FileEntry) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { x, y, targetEntry, currentDirectory, onNewFile, onNewFolder, onDelete, onClose }: Props =
|
||||
$props();
|
||||
|
||||
function handleNewFile() {
|
||||
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||
onNewFile(parentPath);
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleNewFolder() {
|
||||
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||
onNewFolder(parentPath);
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (targetEntry) {
|
||||
onDelete(targetEntry);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50"
|
||||
onclick={onClose}
|
||||
oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="absolute z-50 min-w-[160px] rounded-md border border-gray-700 bg-gray-800 py-1 shadow-lg"
|
||||
style="left: {x}px; top: {y}px;"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-gray-200 hover:bg-gray-700"
|
||||
onclick={handleNewFile}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
New File
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-gray-200 hover:bg-gray-700"
|
||||
onclick={handleNewFolder}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
New Folder
|
||||
</button>
|
||||
|
||||
{#if targetEntry}
|
||||
<div class="my-1 border-t border-gray-700"></div>
|
||||
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-red-400 hover:bg-gray-700"
|
||||
onclick={handleDelete}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Delete {targetEntry.isDirectory ? "Folder" : "File"}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { FileEntry } from "$lib/types/editor";
|
||||
import { editorStore } from "$lib/stores/editor";
|
||||
import Self from "./FileTreeItem.svelte";
|
||||
|
||||
export let entry: FileEntry;
|
||||
export let depth: number = 0;
|
||||
interface Props {
|
||||
entry: FileEntry;
|
||||
depth?: number;
|
||||
onContextMenu?: (event: MouseEvent, entry: FileEntry) => void;
|
||||
}
|
||||
|
||||
let { entry, depth = 0, onContextMenu }: Props = $props();
|
||||
|
||||
function handleClick() {
|
||||
if (entry.isDirectory) {
|
||||
@@ -20,9 +26,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: isExpanded = entry.isExpanded ?? false;
|
||||
$: isLoading = entry.isLoading ?? false;
|
||||
$: children = entry.children ?? [];
|
||||
function handleContextMenu(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onContextMenu?.(event, entry);
|
||||
}
|
||||
|
||||
const isExpanded = $derived(entry.isExpanded ?? false);
|
||||
const isLoading = $derived(entry.isLoading ?? false);
|
||||
const children = $derived(entry.children ?? []);
|
||||
</script>
|
||||
|
||||
<div class="file-tree-item">
|
||||
@@ -31,8 +43,9 @@
|
||||
class:directory={entry.isDirectory}
|
||||
class:file={!entry.isDirectory}
|
||||
style="padding-left: {depth * 16 + 8}px"
|
||||
on:click={handleClick}
|
||||
on:keydown={handleKeydown}
|
||||
onclick={handleClick}
|
||||
onkeydown={handleKeydown}
|
||||
oncontextmenu={handleContextMenu}
|
||||
title={entry.path}
|
||||
>
|
||||
{#if entry.isDirectory}
|
||||
@@ -109,7 +122,7 @@
|
||||
{#if entry.isDirectory && isExpanded && children.length > 0}
|
||||
<div class="children">
|
||||
{#each children as child (child.path)}
|
||||
<svelte:self entry={child} depth={depth + 1} />
|
||||
<Self entry={child} depth={depth + 1} {onContextMenu} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
onConfirm: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
placeholder = "",
|
||||
confirmText = "Create",
|
||||
cancelText = "Cancel",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: Props = $props();
|
||||
|
||||
let inputValue = $state("");
|
||||
let inputElement: HTMLInputElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
const trimmed = inputValue.trim();
|
||||
if (trimmed) {
|
||||
onConfirm(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
onCancel();
|
||||
} else if (event.key === "Enter") {
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onclick={onCancel}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-md rounded-lg border border-gray-700 bg-gray-800 p-6 shadow-xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 class="mb-4 text-lg font-semibold text-gray-100">{title}</h2>
|
||||
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
bind:value={inputValue}
|
||||
type="text"
|
||||
{placeholder}
|
||||
class="mb-6 w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-sm text-gray-100 placeholder-gray-400 focus:border-pink-500 focus:outline-none focus:ring-1 focus:ring-pink-500"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
class="rounded-md border border-gray-600 bg-gray-700 px-4 py-2 text-sm text-gray-200 hover:bg-gray-600"
|
||||
onclick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md bg-pink-600 px-4 py-2 text-sm text-white hover:bg-pink-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onclick={handleSubmit}
|
||||
disabled={!inputValue.trim()}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -246,6 +246,100 @@ function createEditorStore() {
|
||||
isEditorVisible.update((v) => !v);
|
||||
}
|
||||
|
||||
async function createFile(parentPath: string, fileName: string): Promise<boolean> {
|
||||
const filePath = `${parentPath}/${fileName}`;
|
||||
try {
|
||||
await invoke("create_file", { path: filePath });
|
||||
// Refresh the parent directory
|
||||
await refreshDirectory(parentPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to create file:", error);
|
||||
saveError.set(`Failed to create file: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createDirectory(parentPath: string, dirName: string): Promise<boolean> {
|
||||
const dirPath = `${parentPath}/${dirName}`;
|
||||
try {
|
||||
await invoke("create_directory", { path: dirPath });
|
||||
// Refresh the parent directory
|
||||
await refreshDirectory(parentPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to create directory:", error);
|
||||
saveError.set(`Failed to create directory: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await invoke("delete_file", { path: filePath });
|
||||
// Close the tab if it's open
|
||||
const currentState = get(state);
|
||||
const openTab = currentState.tabs.find((t) => t.filePath === filePath);
|
||||
if (openTab) {
|
||||
closeTab(openTab.id);
|
||||
}
|
||||
// Refresh the parent directory
|
||||
const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
|
||||
await refreshDirectory(parentPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete file:", error);
|
||||
saveError.set(`Failed to delete file: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDirectory(dirPath: string): Promise<boolean> {
|
||||
try {
|
||||
await invoke("delete_directory", { path: dirPath });
|
||||
// Close any tabs that are in this directory
|
||||
const currentState = get(state);
|
||||
const tabsToClose = currentState.tabs.filter((t) => t.filePath.startsWith(dirPath + "/"));
|
||||
tabsToClose.forEach((tab) => closeTab(tab.id));
|
||||
// Refresh the parent directory
|
||||
const parentPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
|
||||
await refreshDirectory(parentPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete directory:", error);
|
||||
saveError.set(`Failed to delete directory: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDirectory(dirPath: string) {
|
||||
const currentState = get(state);
|
||||
|
||||
// If refreshing the root directory, reload the entire tree
|
||||
if (dirPath === currentState.currentDirectory) {
|
||||
const entries = await loadDirectory(dirPath);
|
||||
state.update((s) => ({ ...s, fileTree: entries }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the specific directory in the tree
|
||||
const children = await loadDirectory(dirPath);
|
||||
state.update((s) => {
|
||||
const updateTree = (entries: FileEntry[]): FileEntry[] => {
|
||||
return entries.map((e) => {
|
||||
if (e.path === dirPath) {
|
||||
return { ...e, children, isExpanded: true };
|
||||
}
|
||||
if (e.children) {
|
||||
return { ...e, children: updateTree(e.children) };
|
||||
}
|
||||
return e;
|
||||
});
|
||||
};
|
||||
return { ...s, fileTree: updateTree(s.fileTree) };
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor() {
|
||||
isEditorVisible.set(true);
|
||||
}
|
||||
@@ -270,6 +364,11 @@ function createEditorStore() {
|
||||
toggleEditor,
|
||||
showEditor,
|
||||
hideEditor,
|
||||
createFile,
|
||||
createDirectory,
|
||||
deleteFile,
|
||||
deleteDirectory,
|
||||
refreshDirectory,
|
||||
|
||||
tabs: derived(state, ($state) => $state.tabs),
|
||||
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+N - New file (when editor is visible)
|
||||
// Note: This just emits an event that FileBrowser listens to
|
||||
if (event.ctrlKey && event.key === "n" && get(editorStore.isEditorVisible)) {
|
||||
event.preventDefault();
|
||||
// Dispatch a custom event that FileBrowser will listen to
|
||||
window.dispatchEvent(new CustomEvent("editor-new-file"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInterrupt() {
|
||||
|
||||
Reference in New Issue
Block a user