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(())
}
#[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::*;
+1
View File
@@ -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");
+23 -1
View File
@@ -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}
+8 -1
View File
@@ -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();
}
}
});
+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() {
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)),