generated from nhcarrigan/template
feat: add rename functionality to file editor
Add ability to rename files and folders through the context menu: - Add rename_path Tauri command in backend - Add renamePath function to editor store that updates open tabs - Add rename option to context menu with pencil icon - Update InputDialog to support initial value for rename operations
This commit is contained in:
@@ -535,6 +535,27 @@ pub async fn delete_directory(path: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_path(old_path: String, new_path: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let old = Path::new(&old_path);
|
||||
let new = Path::new(&new_path);
|
||||
|
||||
if !old.exists() {
|
||||
return Err("Path does not exist".to_string());
|
||||
}
|
||||
|
||||
if new.exists() {
|
||||
return Err("Destination already exists".to_string());
|
||||
}
|
||||
|
||||
fs::rename(old, new).map_err(|e| format!("Failed to rename: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -158,6 +158,7 @@ pub fn run() {
|
||||
create_directory,
|
||||
delete_file,
|
||||
delete_directory,
|
||||
rename_path,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
// Dialog state
|
||||
let dialog = $state<{
|
||||
type: "newFile" | "newFolder" | "delete" | null;
|
||||
type: "newFile" | "newFolder" | "delete" | "rename" | null;
|
||||
parentPath: string;
|
||||
targetEntry: FileEntry | null;
|
||||
}>({
|
||||
@@ -81,6 +81,10 @@
|
||||
dialog = { type: "delete", parentPath: "", targetEntry: entry };
|
||||
}
|
||||
|
||||
function openRenameDialog(entry: FileEntry) {
|
||||
dialog = { type: "rename", parentPath: "", targetEntry: entry };
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
dialog = { type: null, parentPath: "", targetEntry: null };
|
||||
}
|
||||
@@ -106,6 +110,12 @@
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
async function handleRename(newName: string) {
|
||||
if (!dialog.targetEntry) return;
|
||||
await editorStore.renamePath(dialog.targetEntry.path, newName);
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
openNewFileDialog($currentDirectory);
|
||||
}
|
||||
@@ -211,6 +221,7 @@
|
||||
currentDirectory={$currentDirectory}
|
||||
onNewFile={openNewFileDialog}
|
||||
onNewFolder={openNewFolderDialog}
|
||||
onRename={openRenameDialog}
|
||||
onDelete={openDeleteDialog}
|
||||
onClose={closeContextMenu}
|
||||
/>
|
||||
@@ -250,6 +261,17 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if dialog.type === "rename" && dialog.targetEntry}
|
||||
<InputDialog
|
||||
title="Rename {dialog.targetEntry.isDirectory ? 'Folder' : 'File'}"
|
||||
placeholder="Enter new name..."
|
||||
confirmText="Rename"
|
||||
initialValue={dialog.targetEntry.name}
|
||||
onConfirm={handleRename}
|
||||
onCancel={closeDialog}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.file-browser {
|
||||
display: flex;
|
||||
|
||||
@@ -8,12 +8,22 @@
|
||||
currentDirectory: string;
|
||||
onNewFile: (parentPath: string) => void;
|
||||
onNewFolder: (parentPath: string) => void;
|
||||
onRename: (entry: FileEntry) => void;
|
||||
onDelete: (entry: FileEntry) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { x, y, targetEntry, currentDirectory, onNewFile, onNewFolder, onDelete, onClose }: Props =
|
||||
$props();
|
||||
let {
|
||||
x,
|
||||
y,
|
||||
targetEntry,
|
||||
currentDirectory,
|
||||
onNewFile,
|
||||
onNewFolder,
|
||||
onRename,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: Props = $props();
|
||||
|
||||
function handleNewFile() {
|
||||
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||
@@ -27,6 +37,13 @@
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleRename() {
|
||||
if (targetEntry) {
|
||||
onRename(targetEntry);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (targetEntry) {
|
||||
onDelete(targetEntry);
|
||||
@@ -91,6 +108,21 @@
|
||||
{#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-gray-200 hover:bg-gray-700"
|
||||
onclick={handleRename}
|
||||
>
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
Rename
|
||||
</button>
|
||||
|
||||
<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}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
placeholder?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
initialValue?: string;
|
||||
onConfirm: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -13,16 +14,22 @@
|
||||
placeholder = "",
|
||||
confirmText = "Create",
|
||||
cancelText = "Cancel",
|
||||
initialValue = "",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: Props = $props();
|
||||
|
||||
let inputValue = $state("");
|
||||
// svelte-ignore state_referenced_locally - intentionally capture initial value once
|
||||
let inputValue = $state(initialValue);
|
||||
let inputElement: HTMLInputElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
if (inputElement) {
|
||||
inputElement.focus();
|
||||
// Select all text for rename operations
|
||||
if (initialValue) {
|
||||
inputElement.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -340,6 +340,47 @@ function createEditorStore() {
|
||||
});
|
||||
}
|
||||
|
||||
async function renamePath(oldPath: string, newName: string): Promise<boolean> {
|
||||
const parentPath = oldPath.substring(0, oldPath.lastIndexOf("/"));
|
||||
const newPath = `${parentPath}/${newName}`;
|
||||
|
||||
try {
|
||||
await invoke("rename_path", { oldPath, newPath });
|
||||
|
||||
// Update any open tabs that reference this path
|
||||
state.update((s) => ({
|
||||
...s,
|
||||
tabs: s.tabs.map((t) => {
|
||||
if (t.filePath === oldPath) {
|
||||
// Exact match - this file was renamed
|
||||
return {
|
||||
...t,
|
||||
filePath: newPath,
|
||||
fileName: newName,
|
||||
};
|
||||
}
|
||||
if (t.filePath.startsWith(oldPath + "/")) {
|
||||
// File is inside a renamed directory
|
||||
const relativePath = t.filePath.substring(oldPath.length);
|
||||
return {
|
||||
...t,
|
||||
filePath: newPath + relativePath,
|
||||
};
|
||||
}
|
||||
return t;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Refresh the parent directory
|
||||
await refreshDirectory(parentPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to rename:", error);
|
||||
saveError.set(`Failed to rename: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function showEditor() {
|
||||
isEditorVisible.set(true);
|
||||
}
|
||||
@@ -369,6 +410,7 @@ function createEditorStore() {
|
||||
deleteFile,
|
||||
deleteDirectory,
|
||||
refreshDirectory,
|
||||
renamePath,
|
||||
|
||||
tabs: derived(state, ($state) => $state.tabs),
|
||||
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
|
||||
|
||||
Reference in New Issue
Block a user