diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index ae28a9ba02c2..8c17be861a82 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -78,4 +78,5 @@ | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:run-shell-command`, `:sh` | Run a shell command | | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | +| `:rename`, `:rnm` | Rename the currently selected buffer | | `:clear-register` | Clear given register. If no argument is provided, clear all registers. | diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 840e73828dc7..2b5fecd3ee00 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -442,6 +442,11 @@ impl Client { execute_command: Some(lsp::DynamicRegistrationClientCapabilities { dynamic_registration: Some(false), }), + file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities { + will_rename: Some(true), + did_rename: Some(true), + ..Default::default() + }), inlay_hint: Some(lsp::InlayHintWorkspaceClientCapabilities { refresh_support: Some(false), }), @@ -592,6 +597,25 @@ impl Client { ) } + pub fn will_rename_files( + &self, + params: &Vec, + ) -> impl Future> { + let files = params.to_owned(); + let request = self.call::(lsp::RenameFilesParams { files }); + + async move { + let json = request.await?; + let response: Option = serde_json::from_value(json)?; + Ok(response.unwrap_or_default()) + } + } + + pub fn did_rename_files(&self, params: &[lsp::FileRename]) -> impl Future> { + let files = params.to_owned(); + self.notify::(lsp::RenameFilesParams { files }) + } + pub fn did_change_workspace( &self, added: Vec, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index fe92798baae6..900a2c19643e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::fmt::Write; use std::ops::Deref; @@ -6,6 +7,7 @@ use crate::job::Job; use super::*; use helix_core::{encoding, shellwords::Shellwords}; +use helix_lsp::{lsp, Url}; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::editor::{Action, CloseError, ConfigEvent}; use serde_json::Value; @@ -2167,6 +2169,62 @@ fn reset_diff_change( Ok(()) } +fn rename_buffer( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(args.len() == 1, ":rename takes one argument"); + let new_name = args.first().unwrap(); + + let (_, doc) = current!(cx.editor); + let path = doc + .path() + .ok_or_else(|| anyhow!("Scratch buffers can not be renamed, use :write"))? + let mut path_new = path.clone(); + path_new.set_file_name(OsStr::new(new_name.as_ref())); + if path_new.exists() { + bail!("Destination already exists"); + } + + if let Err(e) = std::fs::rename(&path, &path_new) { + bail!("Could not rename file: {e}"); + } + let (_, doc) = current!(cx.editor); + doc.set_path(Some(path_new.as_path())) + .map_err(|_| anyhow!("File renamed, but could not set path of the document"))?; + + if let Some(lsp_client) = doc.language_server() { + if let Ok(old_uri_str) = Url::from_file_path(&path) { + let old_uri = old_uri_str.to_string(); + if let Ok(new_uri_str) = Url::from_file_path(&path_new) { + let new_uri = new_uri_str.to_string(); + let files = vec![lsp::FileRename { old_uri, new_uri }]; + match helix_lsp::block_on(lsp_client.will_rename_files(&files)) { + Ok(edit) => { + if apply_workspace_edit(cx.editor, helix_lsp::OffsetEncoding::Utf8, &edit) + .is_err() + { + log::error!(":rename command failed to apply edits") + } + } + Err(err) => log::error!("willRename request failed: {err}"), + } + } else { + log::error!(":rename command could not get new path uri") + } + } else { + log::error!(":rename command could not get current path uri") + } + } + cx.editor + .set_status(format!("Renamed file to {}", new_name)); + Ok(()) +} + fn clear_register( cx: &mut compositor::Context, args: &[Cow], @@ -2752,6 +2810,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reset_diff_change, signature: CommandSignature::none(), }, + TypableCommand { + name: "rename", + aliases: &["rnm"], + doc: "Rename the currently selected buffer", + fun: rename_buffer, + signature: CommandSignature::none(), + }, TypableCommand { name: "clear-register", aliases: &[],