Move file into new directory by renaming #9

Closed
opened 2026-09-21 18:54:09 +00:00 by barrettruth · 1 comment
Owner

Original issue: barrettruth/canola.nvim#32
Original author: barrettruth
Original date: 2026-02-23T03:08:30Z

Move a file into a new directory by including the directory name in the rename — e.g. changing foo.txt to newdir/foo.txt creates newdir/ and moves the file into it. Multiple levels deep should work too: a/b/c/file.txt.

Upstream context

stevearc/oil.nvim#117, #289, #675, #707, #708 — various reports of the same missing behaviour.

Root cause

There are two layers to the problem.

Layer 1 — parser rejects the edit. In lua/oil/mutator/parser.lua, when parsing an existing entry (lines beginning with /ID), the parser checks whether the parsed name contains / and immediately errors with Filename cannot contain path separator. The user's edit never reaches the mutation pipeline.

Layer 2 — even if it did, no intermediate dir would be created. When diff.id is set (i.e. it is a rename of an existing entry), create_actions_from_diffs resolves it as a move to parent_url .. diff.name. It does not split the destination path to synthesise create actions for any intermediate directories. The path-splitting logic that does handle nested paths (lines 103–130 in mutator/init.lua) only runs for new entries (diff.id == nil).

Proposed approach

Parser change

Remove the hard error for / in an existing-entry name. Instead, treat it as a valid rename target. The parsed name is allowed to contain / in this context; the mutation pipeline must handle it.

Two sub-cases arise:

  • The destination is inside the same buffer's directory (newdir/file.txt where newdir does not yet exist) — synthesise create directory + move actions.
  • The destination is an existing subdirectory visible in the same buffer — synthesise a move action only.

Mutator change

In create_actions_from_diffs, when processing a diff with diff.id set (an existing-entry rename), check whether diff.name contains a path separator. If it does:

  1. Split the destination path into prefix segments and a final filename.
  2. For each prefix segment, emit a create directory action at the appropriate URL (same deduplication guard as for new entries already exists via seen_creates).
  3. Emit the move action with dest_url pointing to the full resolved path.

The topological sort in enforce_action_order already ensures create actions for parent directories are ordered before the move that depends on them (dest_trie:accum_first_parents_of handles this for move actions), so no changes to trie.lua or the ordering logic are required.

new_dir_mode behaviour

The create directory actions synthesised by a path-separator rename go through the normal perform_action path in adapters/files.lua, which already calls uv.fs_mkdir(path, config.new_dir_mode, ...). No special handling is needed; user-configured new_dir_mode is respected automatically.

Edge cases

  • Multiple levels deep (a/b/c/file.txt): handled by splitting on / and emitting one create per segment, same as the existing new-entry logic.
  • Destination already exists as a directory: the create action will fail at the adapter level with a "file exists" error. This is the correct behaviour — the user should use newdir/ (trailing slash) to move into an existing directory.
  • Destination already exists as a file: the move action will overwrite it, consistent with existing move semantics.
  • Name conflicts after the move: the duplicate-name check in the parser operates on the name visible in the current buffer. A name like newdir/foo.txt is unique by that check even if another entry is named foo.txt. The real conflict check happens at the adapter level.
  • Move loops: the cycle detection in enforce_action_order operates on full URLs, so moving a/b to b/a is handled correctly regardless of whether intermediate directories are synthesised.
  • SSH and S3 adapters: both implement perform_action for create and move. The synthesised actions use the same URL scheme as the buffer, so no adapter-specific changes are needed.

Scope

  • Single-level new directories: newdir/file.txt
  • Nested new directories: a/b/c/file.txt
  • Works for files and directories (moving a directory into a new parent)
  • Mutation pipeline integration: create actions for intermediate dirs are ordered before the move
> Original issue: barrettruth/canola.nvim#32 > Original author: `barrettruth` > Original date: 2026-02-23T03:08:30Z Move a file into a new directory by including the directory name in the rename — e.g. changing `foo.txt` to `newdir/foo.txt` creates `newdir/` and moves the file into it. Multiple levels deep should work too: `a/b/c/file.txt`. ## Upstream context stevearc/oil.nvim#117, #289, #675, #707, #708 — various reports of the same missing behaviour. ## Root cause There are two layers to the problem. **Layer 1 — parser rejects the edit.** In `lua/oil/mutator/parser.lua`, when parsing an existing entry (lines beginning with `/ID`), the parser checks whether the parsed name contains `/` and immediately errors with `Filename cannot contain path separator`. The user's edit never reaches the mutation pipeline. **Layer 2 — even if it did, no intermediate dir would be created.** When `diff.id` is set (i.e. it is a rename of an existing entry), `create_actions_from_diffs` resolves it as a move to `parent_url .. diff.name`. It does not split the destination path to synthesise `create` actions for any intermediate directories. The path-splitting logic that does handle nested paths (lines 103–130 in `mutator/init.lua`) only runs for new entries (`diff.id == nil`). ## Proposed approach ### Parser change Remove the hard error for `/` in an existing-entry name. Instead, treat it as a valid rename target. The parsed name is allowed to contain `/` in this context; the mutation pipeline must handle it. Two sub-cases arise: - The destination is inside the same buffer's directory (`newdir/file.txt` where `newdir` does not yet exist) — synthesise `create directory` + `move` actions. - The destination is an existing subdirectory visible in the same buffer — synthesise a `move` action only. ### Mutator change In `create_actions_from_diffs`, when processing a `diff` with `diff.id` set (an existing-entry rename), check whether `diff.name` contains a path separator. If it does: 1. Split the destination path into prefix segments and a final filename. 2. For each prefix segment, emit a `create directory` action at the appropriate URL (same deduplication guard as for new entries already exists via `seen_creates`). 3. Emit the `move` action with `dest_url` pointing to the full resolved path. The topological sort in `enforce_action_order` already ensures `create` actions for parent directories are ordered before the `move` that depends on them (`dest_trie:accum_first_parents_of` handles this for move actions), so no changes to `trie.lua` or the ordering logic are required. ### `new_dir_mode` behaviour The `create directory` actions synthesised by a path-separator rename go through the normal `perform_action` path in `adapters/files.lua`, which already calls `uv.fs_mkdir(path, config.new_dir_mode, ...)`. No special handling is needed; user-configured `new_dir_mode` is respected automatically. ## Edge cases - **Multiple levels deep** (`a/b/c/file.txt`): handled by splitting on `/` and emitting one `create` per segment, same as the existing new-entry logic. - **Destination already exists as a directory**: the `create` action will fail at the adapter level with a "file exists" error. This is the correct behaviour — the user should use `newdir/` (trailing slash) to move into an existing directory. - **Destination already exists as a file**: the `move` action will overwrite it, consistent with existing move semantics. - **Name conflicts after the move**: the duplicate-name check in the parser operates on the name visible in the current buffer. A name like `newdir/foo.txt` is unique by that check even if another entry is named `foo.txt`. The real conflict check happens at the adapter level. - **Move loops**: the cycle detection in `enforce_action_order` operates on full URLs, so moving `a/b` to `b/a` is handled correctly regardless of whether intermediate directories are synthesised. - **SSH and S3 adapters**: both implement `perform_action` for `create` and `move`. The synthesised actions use the same URL scheme as the buffer, so no adapter-specific changes are needed. ## Scope - Single-level new directories: `newdir/file.txt` - Nested new directories: `a/b/c/file.txt` - Works for files and directories (moving a directory into a new parent) - Mutation pipeline integration: `create` actions for intermediate dirs are ordered before the `move`
barrettruth added this to the v1.1 milestone 2026-09-21 18:54:09 +00:00
Author
Owner

Original comment: barrettruth/canola.nvim#32, comment 4101543935
Original author: barrettruth
Original date: 2026-03-21T00:08:27Z

Implemented in #222.

> Original comment: barrettruth/canola.nvim#32, comment 4101543935 > Original author: `barrettruth` > Original date: 2026-03-21T00:08:27Z Implemented in #222.
Sign in to join this conversation.
No description provided.