Repository navigation
How File Operations Work
New File, New Folder, Cut, Copy, Paste, Duplicate, Rename, Move to Trash and drag and drop in the Files panel. The user side is in File Operations; the tree itself is in How the Files panel works.
People expect to manage files where they browse them. Without it they switch to the Finder or a terminal for every rename, and the app then has to guess what happened to their open tabs.
sequenceDiagram
participant User
participant Tree as explorerOps.ts
participant Store as repoStore
participant Api as api.ts
participant Rust as commands/file_ops.rs, file_ops.rs
User->>Tree: F2 on cart.ts, types basket.ts
Tree->>Store: checkUnsaved([cart.ts])
Store-->>Tree: true (no unsaved edits)
Tree->>Api: fileRename(workspaceRoots, path, "basket.ts")
Api->>Rust: file_rename
Rust->>Rust: inside a workspace folder, not a root, not .git, name free
Rust-->>Tree: new absolute path
Tree->>Store: retargetTabs([{from, to}])
Tree->>Tree: applyMoves: expanded folders, selection, clipboard follow
Tree->>Tree: reload the parent folder, filesWritten
Every operation follows the same steps:
-
Pick the targets.
selectedEntries()gives the visible selected rows, top down.topLevel(selection.ts) drops rows inside another selected folder, andmovableTargets(fileOps.ts) drops workspace roots and deleted rows. -
Guard unsaved edits. Rename, move and trash call
repoStore.checkUnsaved(paths). It usesfileTabsUnder(tabs.ts) to find dirty tabs of those files or of files inside those folders, and shows "Save or revert cart.ts first". -
Ask. Names come from
dialogs.promptwithvalidate: nameProblem(fileNames.ts). Rename passesselection: renameSelection(name, isDir), whichDialogHost.svelteapplies withsetSelectionRange. Trash asks withdialogs.confirm({ danger: true }); a drag and drop move asks while theconfirmDragAndDropsetting is on. Before that question, the backend's dry run (api.fileMoveClash) refuses a move onto a taken name, checking the folder on disk. -
Write. The
fileCreate,fileRename,fileCopy,fileMoveandfileTrashwrappers inapi.tstake absolute paths plus the workspace roots. The backend checks every path again; see Commands: app and tools. -
Follow up.
applyMovesre-points the tabs (repoStore.retargetTabs, which usesretargetTabsintabs.ts), the clipboard, the expanded folders and the selection withmovedPath(workspacePaths.ts). After a trash,repoStore.closeTabsUndercloses the tabs.showEntriesopens the folders down to new entries and selects them.afterWritereloads the touched folders at once and callsrepoStore.filesWritten, so git status and the tree refresh without waiting for the watcher.
selection.ts holds the multi-selection: paths, the Shift anchor and the keyboard focus. selectRange and extendSelection use order, the visible rows. The effect that follows the open tab only replaces the selection when the open file is not already in it, so a moved group stays selected while its tabs re-point.
fileOpGroups decides the menu: everything for one row, New File, New Folder and Paste for a workspace root, Cut, Copy, Paste, Move to Trash and Copy Paths for several rows. fileKeyOp maps key presses to the same operations, with Cmd on macOS and Ctrl elsewhere. The tree's onKeydown calls preventDefault, so the native Edit menu's Cut, Copy and Paste do not run too (see Menu keys and routing).
The clipboard (fileClipboard.svelte.ts) holds only a mode and paths. It outlives the panel being hidden, and follows moves and trashes.
flowchart TD
Down["pointerdown on a row"] --> Far{"moved 4 px?"}
Far -->|no| Click["normal click"]
Far -->|yes| Drag["Tree drag: the selection or the row"]
Finder["Tauri onDragDropEvent from the Finder"] --> Point["dropPointToCss"]
Point --> Outside["tree.dragFromOutside"]
Drag --> Target["treeDropTarget: a folder, a file's folder, or the top"]
Outside --> Target
Target --> Show["tint, open a hovered folder after 600 ms, auto-scroll"]
Show --> Drop{"drop"}
Drop -->|row, Option| CopyIt["ops.copy"]
Drop -->|row| MoveIt["ops.move, asks first"]
Drop -->|Finder| CopyIt
The UI library's Tree drags rows with pointer events, not HTML5 drag and drop. Tauri's window takes native drags to report Finder files, and its macOS handler answers every native drag itself, so HTML5 drop events may never reach the page. Pointer events always do, and give us Option for copy, Escape to cancel and the pointer label for free (logic/treeDragger.svelte.ts). Deleted rows and workspace folders cannot be dragged (canDrag), and checkDrop refuses a folder into itself before anything moves.
Finder drops come from getCurrentWebview().onDragDropEvent (explorerFinder.ts), registered on mount and removed on destroy. dropPointToCss turns the position into CSS pixels (wry reports macOS positions in points already, other platforms in device pixels), and the Tree's dragFromOutside shows the same target, hover and scrolling as a row drag.
The MCP UI tools create_file, create_folder, rename_path, copy_paths, move_paths and trash_paths (see How MCP and the CLI work) live in src/lib/mcp/fileTools.ts. They take the same steps without the dialogs: folderFor refuses paths outside the workspace folders, topLevel and checkDrop pick and check the sources, unsavedProblem (built on fileTabsUnder) returns the panel's "Save or revert cart.ts first" as the tool's error, and after the write they call repoStore.retargetTabs or closeTabsUnder, the clipboard's follow or forget, and repoStore.filesWritten. The tree reloads through the workspaceVersion effect. handlers.ts passes the real api and stores in as FileToolDeps, so fileTools.test.ts checks every step with fakes. The panel could later call these functions too.
| File | What it does |
|---|---|
src/lib/views/files/FileExplorer.svelte |
Clicks, keys and the drop of a dragged row |
src/lib/views/files/explorerOps.ts, explorerNames.ts
|
The operations: dialogs and calling the backend |
src/lib/views/files/explorerMenu.ts, explorerFinder.ts
|
The menus; Finder drops |
src/lib/views/files/selection.ts |
Multi-selection, topLevel, rowAfterRemoval
|
src/lib/views/files/fileOps.ts |
Menu groups, labels, targetFolder, fileKeyOp
|
src/lib/views/files/fileNames.ts |
nameProblem, renameSelection
|
src/lib/views/files/dragDrop.ts |
checkDrop, dropPointToCss
|
src/lib/views/files/fileClipboard.svelte.ts |
The internal clipboard |
src/lib/stores/tabs.ts, repo.svelte.ts
|
retargetTabs, fileTabsUnder, checkUnsaved, closeTabsUnder
|
src/lib/stores/workspacePaths.ts |
movedPath, pathsUnder
|
src/lib/ui/DialogHost.svelte |
The prompt's initial selection
|
src-tauri/src/file_ops.rs, commands/file_ops.rs
|
The checked file system writes and the Trash |
Plain file system writes, not git. git mv only works for tracked files and fails across repositories. Git finds renames itself when the change is staged.
Refuse unsaved edits instead of moving them. Saving first could write to the old path or overwrite the other side; asking to save or revert is honest and simple.
Never overwrite. A taken name stops the operation before anything moves; a copy gets a "copy" name.
Roots stay put. Workspace folders are what the workspace is made of, so they only take new files and pastes.
A tab re-points, it does not reopen. It keeps its place and preview state; the editor reloads from the new path.
Copy from the Finder, never move. Taking files away from the Finder by surprise would be worse than an extra copy.
-
src/lib/views/files/selection.test.ts: clicks, Cmd and Shift ranges, Shift+arrows,topLevel, re-pointing and the row after a trash. -
src/lib/views/files/fileOps.test.ts: menu groups per selection, targets and every key per platform. -
src/lib/views/files/fileNames.test.ts: name checks and the rename selection. -
src/lib/views/files/dragDrop.test.ts: drop folders, refusals, taken names, the drag threshold, auto-scroll steps and Finder positions. -
src/lib/stores/tabs.test.tsandworkspacePaths.test.ts:retargetTabs,fileTabsUnder,movedPath,pathsUnder. -
src/lib/help/shortcuts.test.ts: the Files panel keys in the shortcuts window. -
src/lib/mcp/fileTools.test.ts: the MCP file tools, with refusals, tab and clipboard follow-ups and refreshes. - The backend: unit tests in
src-tauri/src/file_ops.rsand command tests with real temporary folders insrc-tauri/src/commands/tests.rs.
Dragging, Finder drops, the menus and focus after a dialog need a check in the real app. See Testing.
- A new operation goes in
fileOpGroups,FILE_OP_LABELSand, with a key,fileKeyOp,fileOpAcceleratorand the Files panel section ofsrc/lib/help/shortcuts.ts. - A change to what an operation checks or follows up should reach
src/lib/mcp/fileTools.tstoo. - Update File Operations, Keyboard Shortcuts and retake
file-ops-menu.pngandfile-ops-rename.pngwhen the menu or the dialogs change.
A move onto a taken name asked first, then failed.
-
The issue: dragging
cart.tsonto a folder that already had acart.tsasked "Move cart.ts into docs?", and only after Move showed "cart.ts already exists". - Why it happened: only the backend checked names, and it runs after the question.
-
The fix and why we chose it:
moveEntrieschecks the names withmoveClashbefore asking, case-insensitively on macOS and Windows like their file systems. The backend still checks, since the folder can change in between.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode