aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyle Smith2023-03-08 02:11:43 +0000
committerGitHub2023-03-08 02:11:43 +0000
commit44ff8a1df1f69733bc40ea866674fcfd7e0cdded (patch)
tree91e69301836f46a3c0676be821d4ca380212c797
parent3849ca4c2abdb1c6076d391eab4c3f43dda30234 (diff)
LSP: Support textDocument/prepareRename (#6103)
* LSP: Support textDocument/prepareRename 'textDocument/prepareRename' can be used by the client to ask the server the range of the symbol under the cursor which would be changed by a subsequent call to 'textDocument/rename' with that position. We can use this information to fill the prompt with an accurate prefill which can improve the UX for renaming symbols when the symbol doesn't align with the "word" textobject. (We currently use the "word" textobject as a default value for the prompt.) Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * clippy fixes * rustfmt * Update helix-term/src/commands/lsp.rs Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * Update helix-term/src/commands/lsp.rs Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * fix clippy from suggestions * Update helix-term/src/commands/lsp.rs Co-authored-by: Michael Davis <mcarsondavis@gmail.com> --------- Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
-rw-r--r--helix-lsp/src/client.rs25
-rw-r--r--helix-term/src/commands/lsp.rs136
2 files changed, 125 insertions, 36 deletions
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 95f3ea34..9fa118fb 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -359,7 +359,7 @@ impl Client {
}),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
- prepare_support: Some(false),
+ prepare_support: Some(true),
prepare_support_default_behavior: None,
honors_change_annotations: Some(false),
}),
@@ -1034,6 +1034,29 @@ impl Client {
Some(self.call::<lsp::request::DocumentSymbolRequest>(params))
}
+ pub fn prepare_rename(
+ &self,
+ text_document: lsp::TextDocumentIdentifier,
+ position: lsp::Position,
+ ) -> Option<impl Future<Output = Result<Value>>> {
+ let capabilities = self.capabilities.get().unwrap();
+
+ match capabilities.rename_provider {
+ Some(lsp::OneOf::Right(lsp::RenameOptions {
+ prepare_provider: Some(true),
+ ..
+ })) => (),
+ _ => return None,
+ }
+
+ let params = lsp::TextDocumentPositionParams {
+ text_document,
+ position,
+ };
+
+ Some(self.call::<lsp::request::PrepareRenameRequest>(params))
+ }
+
// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index d59eebdb..08519366 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -1232,49 +1232,115 @@ pub fn hover(cx: &mut Context) {
}
pub fn rename_symbol(cx: &mut Context) {
- let (view, doc) = current_ref!(cx.editor);
- let text = doc.text().slice(..);
- let primary_selection = doc.selection(view.id).primary();
- let prefill = if primary_selection.len() > 1 {
- primary_selection
- } else {
- use helix_core::textobject::{textobject_word, TextObject};
- textobject_word(text, primary_selection, TextObject::Inside, 1, false)
+ fn get_prefill_from_word_boundary(editor: &Editor) -> String {
+ let (view, doc) = current_ref!(editor);
+ let text = doc.text().slice(..);
+ let primary_selection = doc.selection(view.id).primary();
+ if primary_selection.len() > 1 {
+ primary_selection
+ } else {
+ use helix_core::textobject::{textobject_word, TextObject};
+ textobject_word(text, primary_selection, TextObject::Inside, 1, false)
+ }
+ .fragment(text)
+ .into()
}
- .fragment(text)
- .into();
- ui::prompt_with_input(
- cx,
- "rename-to:".into(),
- prefill,
- None,
- ui::completers::none,
- move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
- if event != PromptEvent::Validate {
- return;
+
+ fn get_prefill_from_lsp_response(
+ editor: &Editor,
+ offset_encoding: OffsetEncoding,
+ response: Option<lsp::PrepareRenameResponse>,
+ ) -> Result<String, &'static str> {
+ match response {
+ Some(lsp::PrepareRenameResponse::Range(range)) => {
+ let text = doc!(editor).text();
+
+ Ok(lsp_range_to_range(text, range, offset_encoding)
+ .ok_or("lsp sent invalid selection range for rename")?
+ .fragment(text.slice(..))
+ .into())
+ }
+ Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { placeholder, .. }) => {
+ Ok(placeholder)
+ }
+ Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => {
+ Ok(get_prefill_from_word_boundary(editor))
}
+ None => Err("lsp did not respond to prepare rename request"),
+ }
+ }
- let (view, doc) = current!(cx.editor);
- let language_server = language_server!(cx.editor, doc);
- let offset_encoding = language_server.offset_encoding();
+ fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
+ let prompt = ui::Prompt::new(
+ "rename-to:".into(),
+ None,
+ ui::completers::none,
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ if event != PromptEvent::Validate {
+ return;
+ }
- let pos = doc.position(view.id, offset_encoding);
+ let (view, doc) = current!(cx.editor);
+ let language_server = language_server!(cx.editor, doc);
+ let offset_encoding = language_server.offset_encoding();
+
+ let pos = doc.position(view.id, offset_encoding);
+
+ let future =
+ match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
+ Some(future) => future,
+ None => {
+ cx.editor
+ .set_error("Language server does not support symbol renaming");
+ return;
+ }
+ };
+ match block_on(future) {
+ Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
+ Err(err) => cx.editor.set_error(err.to_string()),
+ }
+ },
+ )
+ .with_line(prefill, editor);
- let future =
- match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
- Some(future) => future,
- None => {
- cx.editor
- .set_error("Language server does not support symbol renaming");
+ Box::new(prompt)
+ }
+
+ let (view, doc) = current!(cx.editor);
+ let language_server = language_server!(cx.editor, doc);
+ let offset_encoding = language_server.offset_encoding();
+
+ let pos = doc.position(view.id, offset_encoding);
+
+ match language_server.prepare_rename(doc.identifier(), pos) {
+ // Language server supports textDocument/prepareRename, use it.
+ Some(future) => cx.callback(
+ future,
+ move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
+ let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
+ {
+ Ok(p) => p,
+ Err(e) => {
+ editor.set_error(e);
return;
}
};
- match block_on(future) {
- Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
- Err(err) => cx.editor.set_error(err.to_string()),
- }
- },
- );
+
+ let prompt = create_rename_prompt(editor, prefill);
+
+ compositor.push(prompt);
+ },
+ ),
+ // Language server does not support textDocument/prepareRename, fall back
+ // to word boundary selection.
+ None => {
+ let prefill = get_prefill_from_word_boundary(cx.editor);
+
+ let prompt = create_rename_prompt(cx.editor, prefill);
+
+ cx.push_layer(prompt);
+ }
+ };
}
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {