Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move directories while updating buffer paths (supercharged :move) #12923

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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