feat: add rename functionality to file editor
CI / Lint & Test (pull_request) Failing after 6m40s
CI / Build Linux (pull_request) Has been skipped
CI / Build Windows (cross-compile) (pull_request) Has been skipped
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m23s

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:
2026-01-28 16:49:29 -08:00
committed by Naomi Carrigan
parent abacb0131f
commit 505e24cbd2
6 changed files with 129 additions and 4 deletions
+21
View File
@@ -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::*;
+1
View File
@@ -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");
+23 -1
View File
@@ -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}
+8 -1
View File
@@ -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();
}
} }
}); });
+42
View File
@@ -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)),