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>
pull/5682/head^2
Kyle Smith 2 years ago committed by GitHub
parent 3849ca4c2a
commit 44ff8a1df1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -359,7 +359,7 @@ impl Client {
}), }),
rename: Some(lsp::RenameClientCapabilities { rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false), dynamic_registration: Some(false),
prepare_support: Some(false), prepare_support: Some(true),
prepare_support_default_behavior: None, prepare_support_default_behavior: None,
honors_change_annotations: Some(false), honors_change_annotations: Some(false),
}), }),
@ -1034,6 +1034,29 @@ impl Client {
Some(self.call::<lsp::request::DocumentSymbolRequest>(params)) 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 // empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> { pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap(); let capabilities = self.capabilities.get().unwrap();

@ -1232,21 +1232,47 @@ pub fn hover(cx: &mut Context) {
} }
pub fn rename_symbol(cx: &mut Context) { pub fn rename_symbol(cx: &mut Context) {
let (view, doc) = current_ref!(cx.editor); fn get_prefill_from_word_boundary(editor: &Editor) -> String {
let (view, doc) = current_ref!(editor);
let text = doc.text().slice(..); let text = doc.text().slice(..);
let primary_selection = doc.selection(view.id).primary(); let primary_selection = doc.selection(view.id).primary();
let prefill = if primary_selection.len() > 1 { if primary_selection.len() > 1 {
primary_selection primary_selection
} else { } else {
use helix_core::textobject::{textobject_word, TextObject}; use helix_core::textobject::{textobject_word, TextObject};
textobject_word(text, primary_selection, TextObject::Inside, 1, false) textobject_word(text, primary_selection, TextObject::Inside, 1, false)
} }
.fragment(text) .fragment(text)
.into(); .into()
ui::prompt_with_input( }
cx,
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"),
}
}
fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
let prompt = ui::Prompt::new(
"rename-to:".into(), "rename-to:".into(),
prefill,
None, None,
ui::completers::none, ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
@ -1274,7 +1300,47 @@ pub fn rename_symbol(cx: &mut Context) {
Err(err) => cx.editor.set_error(err.to_string()), Err(err) => cx.editor.set_error(err.to_string()),
} }
}, },
); )
.with_line(prefill, editor);
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;
}
};
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) { pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {

Loading…
Cancel
Save