aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYomain2023-11-08 18:38:17 +0000
committerGitHub2023-11-08 18:38:17 +0000
commite868678139bc8f4f5b14236c3c3c3f6aa4aab47b (patch)
tree2da8187e5323cd8ad7b2df2cbe9a44ed7951247d
parent7bc564d3dcdee69555578bbec75d08e6ce227a9e (diff)
Add command to move files with LSP support (#8584)
* 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>
-rw-r--r--book/src/generated/typable-cmd.md1
-rw-r--r--helix-lsp/src/client.rs76
-rw-r--r--helix-term/src/commands/typed.rs84
3 files changed, 159 insertions, 2 deletions
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
index 4b737893..6280d3c7 100644
--- a/book/src/generated/typable-cmd.md
+++ b/book/src/generated/typable-cmd.md
@@ -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 |
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 341702c3..e6e1f8a0 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -404,9 +404,19 @@ impl Client {
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;
@@ -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 {
@@ -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
// -------------------------------------------------------------------------------------------
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index e7343308..4148257f 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -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;
@@ -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",
@@ -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>> =