Skip to content

Commit

Permalink
feat: moving multiple files/directories
Browse files Browse the repository at this point in the history
  • Loading branch information
satoqz committed Feb 28, 2025
1 parent 0efa820 commit 5e5766a
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 22 deletions.
2 changes: 1 addition & 1 deletion book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI |
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
| `:move`, `:mv` | Move files, updating any affected open buffers. A single argument moves the current buffer's file, multiple arguments move multiple source files to a target (same as the Unix `mv` command) |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |
| `:echo` | Prints the given arguments to the statusline. |
Expand Down
58 changes: 46 additions & 12 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use helix_core::command_line::{Args, Flag, Signature, Token, TokenKind};
use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
use helix_core::line_ending;
use helix_stdx::path::home_dir;
use helix_stdx::path::{canonicalize, home_dir};
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
use helix_view::editor::{CloseError, ConfigEvent};
use helix_view::expansion;
Expand Down Expand Up @@ -2395,15 +2395,49 @@ fn move_buffer(cx: &mut compositor::Context, args: Args, event: PromptEvent) ->
return Ok(());
}

let doc = doc!(cx.editor);
let old_path = doc
.path()
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
let new_path = args.first().unwrap().to_string();
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
bail!("Could not move file: {err}");
let mut new_path = canonicalize(&args[args.len() - 1]);
let is_dir = new_path.is_dir();

let mut do_move = |old_path: PathBuf, editor: &mut Editor| {
let file_name = old_path
.file_name()
.context("Cannot move file: source is root directory")?;
// Allow moving files into directories without repeating the file name in the new path.
if is_dir {
new_path.push(file_name);
}
if let Err(err) = editor.move_path(old_path.as_path(), new_path.as_ref()) {
bail!("Could not move file: {err}");
}
if is_dir {
new_path.pop();
}
Ok(())
};

// Move the current buffer.
if args.len() == 1 {
let doc = doc!(cx.editor);
let old_path = doc
.path()
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
return do_move(old_path, cx.editor);
}

// Move multiple files to one destination, like `mv foo bar baz` where "baz" is a directory.
// If we have multiple source files, the destination must be a directory.
if !is_dir && args.len() > 2 {
bail!("Cannot move files: not a directory");
}
for old_path in args
.iter()
.take(args.len() - 1)
.map(|arg| canonicalize(arg.to_string()))
{
do_move(old_path, cx.editor)?;
}

Ok(())
}

Expand Down Expand Up @@ -3441,11 +3475,11 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "move",
aliases: &["mv"],
doc: "Move the current buffer and its corresponding file to a different path",
doc: "Move files, updating any affected open buffers. A single argument moves the current buffer's file, multiple arguments move multiple source files to a target (same as the Unix `mv` command)",
fun: move_buffer,
completer: CommandCompleter::positional(&[completers::filename]),
completer: CommandCompleter::all(completers::filename),
signature: Signature {
positionals: (1, Some(1)),
positionals: (1, None),
..Signature::DEFAULT
},
},
Expand Down
35 changes: 26 additions & 9 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,7 @@ impl Editor {
}

/// moves/renames a path, invoking any event handlers (currently only lsp)
/// and calling `set_doc_path` if the file is open in the editor
/// and calling `set_doc_path` for all affected files open in the editor
pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> {
let new_path = canonicalize(new_path);
// sanity check
Expand Down Expand Up @@ -1406,8 +1406,31 @@ impl Editor {
}
}
fs::rename(old_path, &new_path)?;
if let Some(doc) = self.document_by_path(old_path) {
self.set_doc_path(doc.id(), &new_path);
let mut to_update: Vec<_> = self
.documents
.iter()
.filter_map(|(id, doc)| {
let old_doc_path = doc.path()?;
let relative = old_doc_path.strip_prefix(old_path).ok()?;
let new_doc_path = new_path.join(relative);
Some((old_doc_path.to_owned(), new_doc_path, Some(*id)))
})
.collect();
// If old_path doesn't have an attached document, we haven't included it above.
// We can still tell the language servers that it changed.
if self.document_by_path(old_path).is_none() {
to_update.push((old_path.to_owned(), new_path.clone(), None));
}
for (old_doc_path, new_doc_path, id) in to_update {
if let Some(id) = id {
self.set_doc_path(id, &new_doc_path);
}
self.language_servers
.file_event_handler
.file_changed(old_doc_path);
self.language_servers
.file_event_handler
.file_changed(new_doc_path);
}
let is_dir = new_path.is_dir();
for ls in self.language_servers.iter_clients() {
Expand All @@ -1418,12 +1441,6 @@ impl Editor {
}
ls.did_rename(old_path, &new_path, is_dir);
}
self.language_servers
.file_event_handler
.file_changed(old_path.to_owned());
self.language_servers
.file_event_handler
.file_changed(new_path);
Ok(())
}

Expand Down

0 comments on commit 5e5766a

Please sign in to comment.