diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 219f6b95f..4fe49625a 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -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. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2013a9d81..660c6ecc3 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -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; @@ -2460,15 +2460,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 Some(file_name) = old_path.file_name() else { + bail!("Cannot move this path: {}", old_path.to_string_lossy()); + }; + // 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(()) } @@ -3515,11 +3549,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 }, }, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cb9586e79..b229b7f60 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1408,7 +1408,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 @@ -1438,8 +1438,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() { @@ -1450,12 +1473,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(()) }