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(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ pub fn run() {
|
|||||||
create_directory,
|
create_directory,
|
||||||
delete_file,
|
delete_file,
|
||||||
delete_directory,
|
delete_directory,
|
||||||
|
rename_path,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
// Dialog state
|
// Dialog state
|
||||||
let dialog = $state<{
|
let dialog = $state<{
|
||||||
type: "newFile" | "newFolder" | "delete" | null;
|
type: "newFile" | "newFolder" | "delete" | "rename" | null;
|
||||||
parentPath: string;
|
parentPath: string;
|
||||||
targetEntry: FileEntry | null;
|
targetEntry: FileEntry | null;
|
||||||
}>({
|
}>({
|
||||||
@@ -81,6 +81,10 @@
|
|||||||
dialog = { type: "delete", parentPath: "", targetEntry: entry };
|
dialog = { type: "delete", parentPath: "", targetEntry: entry };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRenameDialog(entry: FileEntry) {
|
||||||
|
dialog = { type: "rename", parentPath: "", targetEntry: entry };
|
||||||
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
dialog = { type: null, parentPath: "", targetEntry: null };
|
dialog = { type: null, parentPath: "", targetEntry: null };
|
||||||
}
|
}
|
||||||
@@ -106,6 +110,12 @@
|
|||||||
closeDialog();
|
closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRename(newName: string) {
|
||||||
|
if (!dialog.targetEntry) return;
|
||||||
|
await editorStore.renamePath(dialog.targetEntry.path, newName);
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
function handleNewFile() {
|
function handleNewFile() {
|
||||||
openNewFileDialog($currentDirectory);
|
openNewFileDialog($currentDirectory);
|
||||||
}
|
}
|
||||||
@@ -211,6 +221,7 @@
|
|||||||
currentDirectory={$currentDirectory}
|
currentDirectory={$currentDirectory}
|
||||||
onNewFile={openNewFileDialog}
|
onNewFile={openNewFileDialog}
|
||||||
onNewFolder={openNewFolderDialog}
|
onNewFolder={openNewFolderDialog}
|
||||||
|
onRename={openRenameDialog}
|
||||||
onDelete={openDeleteDialog}
|
onDelete={openDeleteDialog}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
/>
|
/>
|
||||||
@@ -250,6 +261,17 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/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>
|
<style>
|
||||||
.file-browser {
|
.file-browser {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -8,12 +8,22 @@
|
|||||||
currentDirectory: string;
|
currentDirectory: string;
|
||||||
onNewFile: (parentPath: string) => void;
|
onNewFile: (parentPath: string) => void;
|
||||||
onNewFolder: (parentPath: string) => void;
|
onNewFolder: (parentPath: string) => void;
|
||||||
|
onRename: (entry: FileEntry) => void;
|
||||||
onDelete: (entry: FileEntry) => void;
|
onDelete: (entry: FileEntry) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x, y, targetEntry, currentDirectory, onNewFile, onNewFolder, onDelete, onClose }: Props =
|
let {
|
||||||
$props();
|
x,
|
||||||
|
y,
|
||||||
|
targetEntry,
|
||||||
|
currentDirectory,
|
||||||
|
onNewFile,
|
||||||
|
onNewFolder,
|
||||||
|
onRename,
|
||||||
|
onDelete,
|
||||||
|
onClose,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
function handleNewFile() {
|
function handleNewFile() {
|
||||||
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
const parentPath = targetEntry?.isDirectory ? targetEntry.path : currentDirectory;
|
||||||
@@ -27,6 +37,13 @@
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRename() {
|
||||||
|
if (targetEntry) {
|
||||||
|
onRename(targetEntry);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (targetEntry) {
|
if (targetEntry) {
|
||||||
onDelete(targetEntry);
|
onDelete(targetEntry);
|
||||||
@@ -91,6 +108,21 @@
|
|||||||
{#if targetEntry}
|
{#if targetEntry}
|
||||||
<div class="my-1 border-t border-gray-700"></div>
|
<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
|
<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"
|
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}
|
onclick={handleDelete}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
|
initialValue?: string;
|
||||||
onConfirm: (value: string) => void;
|
onConfirm: (value: string) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
@@ -13,16 +14,22 @@
|
|||||||
placeholder = "",
|
placeholder = "",
|
||||||
confirmText = "Create",
|
confirmText = "Create",
|
||||||
cancelText = "Cancel",
|
cancelText = "Cancel",
|
||||||
|
initialValue = "",
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let inputValue = $state("");
|
// svelte-ignore state_referenced_locally - intentionally capture initial value once
|
||||||
|
let inputValue = $state(initialValue);
|
||||||
let inputElement: HTMLInputElement | undefined = $state();
|
let inputElement: HTMLInputElement | undefined = $state();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
inputElement.focus();
|
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() {
|
function showEditor() {
|
||||||
isEditorVisible.set(true);
|
isEditorVisible.set(true);
|
||||||
}
|
}
|
||||||
@@ -369,6 +410,7 @@ function createEditorStore() {
|
|||||||
deleteFile,
|
deleteFile,
|
||||||
deleteDirectory,
|
deleteDirectory,
|
||||||
refreshDirectory,
|
refreshDirectory,
|
||||||
|
renamePath,
|
||||||
|
|
||||||
tabs: derived(state, ($state) => $state.tabs),
|
tabs: derived(state, ($state) => $state.tabs),
|
||||||
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
|
activeTab: derived(state, ($state) => $state.tabs.find((t) => t.id === $state.activeTabId)),
|
||||||
|
|||||||
Reference in New Issue
Block a user