Skip to content

Commit

Permalink
Add command to move files with LSP support (helix-editor#8584)
Browse files Browse the repository at this point in the history
* Added rename command

* Added an error if the new path already exists

* Fixed wrong command name being used

* fixed clippy suggestions

* removed didRenameFiles call, fixed early return due to path Err

* added ':rnm' alias to ':rename'

* code cleanup

* formatting

* removed debug line

* cargo fmt

* Improved new buffer error message

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Removed unnecessary path normalizing

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* feat: change `rename` command to `move`

* feat: add multi lsp support when moving files

* feat: allow lsp calls with a custom timeout

* feat: sending lsp file_changed event once file has moved

---------

Co-authored-by: ontley <theontley@gmail.com>
Co-authored-by: ontley <67148677+ontley@users.noreply.github.com>
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
  • Loading branch information
4 people authored and Vulpesx committed Jun 7, 2024
1 parent cca272d commit 9ebf808
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 2 deletions.
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@
| `: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` | Move the current buffer and its corresponding file to a different path |
76 changes: 75 additions & 1 deletion helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,12 +401,22 @@ impl Client {
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
self.call_with_timeout::<R>(params, self.req_timeout)
}

fn call_with_timeout<R: lsp::request::Request>(
&self,
params: R::Params,
timeout_secs: u64,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
let timeout_secs = self.req_timeout;

async move {
use std::time::Duration;
Expand Down Expand Up @@ -548,6 +558,11 @@ impl Client {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities {
will_rename: Some(true),
did_rename: Some(true),
..Default::default()
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
Expand Down Expand Up @@ -700,6 +715,65 @@ impl Client {
})
}

pub fn prepare_file_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.capabilities.get().unwrap();

// Return early if the server does not support willRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.will_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
}

let files = vec![lsp::FileRename {
old_uri: old_uri.to_string(),
new_uri: new_uri.to_string(),
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
lsp::RenameFilesParams { files },
5,
);

Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
}

pub fn did_file_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
let capabilities = self.capabilities.get().unwrap();

// Return early if the server does not support DidRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.did_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
}

let files = vec![lsp::FileRename {
old_uri: old_uri.to_string(),
new_uri: new_uri.to_string(),
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
}

// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
Expand Down
84 changes: 83 additions & 1 deletion helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::job::Job;
use super::*;

use helix_core::fuzzy::fuzzy_match;
use helix_core::{encoding, line_ending, shellwords::Shellwords};
use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
use helix_lsp::{OffsetEncoding, Url};
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use serde_json::Value;
Expand Down Expand Up @@ -2408,6 +2409,80 @@ fn redraw(
Ok(())
}

fn move_buffer(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

ensure!(args.len() == 1, format!(":move takes one argument"));
let doc = doc!(cx.editor);

let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
let old_path = doc
.path()
.ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
.clone();
let old_path_as_url = doc.url().unwrap();
let new_path_as_url = Url::from_file_path(&new_path).unwrap();

let edits: Vec<(
helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
OffsetEncoding,
String,
)> = doc
.language_servers()
.map(|lsp| {
(
lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
lsp.offset_encoding(),
lsp.name().to_owned(),
)
})
.filter(|(f, _, _)| f.is_some())
.map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
.collect();

for (lsp_reply, encoding, name) in edits {
match lsp_reply {
Ok(edit) => {
if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
log::error!(
":move command failed to apply edits from lsp {}: {:?}",
name,
e
);
};
}
Err(e) => {
log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
}
};
}

let doc = doc_mut!(cx.editor);

doc.set_path(Some(new_path.as_path()));
if let Err(e) = std::fs::rename(&old_path, &new_path) {
doc.set_path(Some(old_path.as_path()));
bail!("Could not move file: {}", e);
};

doc.language_servers().for_each(|lsp| {
lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
});

cx.editor
.language_servers
.file_event_handler
.file_changed(new_path);

Ok(())
}

pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -3008,6 +3083,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: redraw,
signature: CommandSignature::none(),
},
TypableCommand {
name: "move",
aliases: &[],
doc: "Move the current buffer and its corresponding file to a different path",
fun: move_buffer,
signature: CommandSignature::positional(&[completers::filename]),
},
];

pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Expand Down

0 comments on commit 9ebf808

Please sign in to comment.