on master help sign in
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>

commite868678139bc8f4f5b14236c3c3c3f6aa4aab47b authorYomain <40139584+yo-main@users.noreply.github.com> dateWed 8 Nov 2023, 19:38 (3 years ago) parent7bc564d
3 files changed+159−2
book/src/generated/typable-cmd.md+1 helix-lsp/src/client.rs+75−1 helix-term/src/commands/typed.rs+83−1
book/src/generated/typable-cmd.md+1
@@ -85,3 +85,4 @@
8585| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
8686| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
8787| `:redraw` | Clear and re-render the whole UI |
88+| `:move` | Move the current buffer and its corresponding file to a different path |
helix-lsp/src/client.rs+75−1
@@ -401,12 +401,22 @@ impl Client {
401401 &self,
402402 params: R::Params,
403403 ) -> impl Future<Output = Result<Value>>
404+ where
405+ R::Params: serde::Serialize,
406+ {
407+ self.call_with_timeout::<R>(params, self.req_timeout)
408+ }
409+
410+ fn call_with_timeout<R: lsp::request::Request>(
411+ &self,
412+ params: R::Params,
413+ timeout_secs: u64,
414+ ) -> impl Future<Output = Result<Value>>
404415 where
405416 R::Params: serde::Serialize,
406417 {
407418 let server_tx = self.server_tx.clone();
408419 let id = self.next_request_id();
409− let timeout_secs = self.req_timeout;
410420
411421 async move {
412422 use std::time::Duration;
@@ -548,6 +558,11 @@ impl Client {
548558 dynamic_registration: Some(true),
549559 relative_pattern_support: Some(false),
550560 }),
561+ file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities {
562+ will_rename: Some(true),
563+ did_rename: Some(true),
564+ ..Default::default()
565+ }),
551566 ..Default::default()
552567 }),
553568 text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -700,6 +715,65 @@ impl Client {
700715 })
701716 }
702717
718+ pub fn prepare_file_rename(
719+ &self,
720+ old_uri: &lsp::Url,
721+ new_uri: &lsp::Url,
722+ ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
723+ let capabilities = self.capabilities.get().unwrap();
724+
725+ // Return early if the server does not support willRename feature
726+ match &capabilities.workspace {
727+ Some(workspace) => match &workspace.file_operations {
728+ Some(op) => {
729+ op.will_rename.as_ref()?;
730+ }
731+ _ => return None,
732+ },
733+ _ => return None,
734+ }
735+
736+ let files = vec![lsp::FileRename {
737+ old_uri: old_uri.to_string(),
738+ new_uri: new_uri.to_string(),
739+ }];
740+ let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
741+ lsp::RenameFilesParams { files },
742+ 5,
743+ );
744+
745+ Some(async move {
746+ let json = request.await?;
747+ let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
748+ Ok(response.unwrap_or_default())
749+ })
750+ }
751+
752+ pub fn did_file_rename(
753+ &self,
754+ old_uri: &lsp::Url,
755+ new_uri: &lsp::Url,
756+ ) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
757+ let capabilities = self.capabilities.get().unwrap();
758+
759+ // Return early if the server does not support DidRename feature
760+ match &capabilities.workspace {
761+ Some(workspace) => match &workspace.file_operations {
762+ Some(op) => {
763+ op.did_rename.as_ref()?;
764+ }
765+ _ => return None,
766+ },
767+ _ => return None,
768+ }
769+
770+ let files = vec![lsp::FileRename {
771+ old_uri: old_uri.to_string(),
772+ new_uri: new_uri.to_string(),
773+ }];
774+ Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
775+ }
776+
703777 // -------------------------------------------------------------------------------------------
704778 // Text document
705779 // -------------------------------------------------------------------------------------------
helix-term/src/commands/typed.rs+83−1
@@ -6,7 +6,8 @@ use crate::job::Job;
66use super::*;
77
88use helix_core::fuzzy::fuzzy_match;
9−use helix_core::{encoding, line_ending, shellwords::Shellwords};
9+use helix_core::{encoding, line_ending, path::get_canonicalized_path, shellwords::Shellwords};
10+use helix_lsp::{OffsetEncoding, Url};
1011use helix_view::document::DEFAULT_LANGUAGE_NAME;
1112use helix_view::editor::{Action, CloseError, ConfigEvent};
1213use serde_json::Value;
@@ -2408,6 +2409,80 @@ fn redraw(
24082409 Ok(())
24092410}
24102411
2412+fn move_buffer(
2413+ cx: &mut compositor::Context,
2414+ args: &[Cow<str>],
2415+ event: PromptEvent,
2416+) -> anyhow::Result<()> {
2417+ if event != PromptEvent::Validate {
2418+ return Ok(());
2419+ }
2420+
2421+ ensure!(args.len() == 1, format!(":move takes one argument"));
2422+ let doc = doc!(cx.editor);
2423+
2424+ let new_path = get_canonicalized_path(&PathBuf::from(args.first().unwrap().to_string()));
2425+ let old_path = doc
2426+ .path()
2427+ .ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
2428+ .clone();
2429+ let old_path_as_url = doc.url().unwrap();
2430+ let new_path_as_url = Url::from_file_path(&new_path).unwrap();
2431+
2432+ let edits: Vec<(
2433+ helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
2434+ OffsetEncoding,
2435+ String,
2436+ )> = doc
2437+ .language_servers()
2438+ .map(|lsp| {
2439+ (
2440+ lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
2441+ lsp.offset_encoding(),
2442+ lsp.name().to_owned(),
2443+ )
2444+ })
2445+ .filter(|(f, _, _)| f.is_some())
2446+ .map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
2447+ .collect();
2448+
2449+ for (lsp_reply, encoding, name) in edits {
2450+ match lsp_reply {
2451+ Ok(edit) => {
2452+ if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
2453+ log::error!(
2454+ ":move command failed to apply edits from lsp {}: {:?}",
2455+ name,
2456+ e
2457+ );
2458+ };
2459+ }
2460+ Err(e) => {
2461+ log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
2462+ }
2463+ };
2464+ }
2465+
2466+ let doc = doc_mut!(cx.editor);
2467+
2468+ doc.set_path(Some(new_path.as_path()));
2469+ if let Err(e) = std::fs::rename(&old_path, &new_path) {
2470+ doc.set_path(Some(old_path.as_path()));
2471+ bail!("Could not move file: {}", e);
2472+ };
2473+
2474+ doc.language_servers().for_each(|lsp| {
2475+ lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
2476+ });
2477+
2478+ cx.editor
2479+ .language_servers
2480+ .file_event_handler
2481+ .file_changed(new_path);
2482+
2483+ Ok(())
2484+}
2485+
24112486pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
24122487 TypableCommand {
24132488 name: "quit",
@@ -3008,6 +3083,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
30083083 fun: redraw,
30093084 signature: CommandSignature::none(),
30103085 },
3086+ TypableCommand {
3087+ name: "move",
3088+ aliases: &[],
3089+ doc: "Move the current buffer and its corresponding file to a different path",
3090+ fun: move_buffer,
3091+ signature: CommandSignature::positional(&[completers::filename]),
3092+ },
30113093];
30123094
30133095pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =