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

commit44ff8a1df1f69733bc40ea866674fcfd7e0cdded authorKyle Smith <askreet@gmail.com> dateTue 7 Mar 2023, 21:11 (4 years ago) parent3849ca4
2 files changed+125−36
helix-lsp/src/client.rs+24−1 helix-term/src/commands/lsp.rs+101−35
helix-lsp/src/client.rs+24−1
@@ -359,7 +359,7 @@ impl Client {
359359 }),
360360 rename: Some(lsp::RenameClientCapabilities {
361361 dynamic_registration: Some(false),
362− prepare_support: Some(false),
362+ prepare_support: Some(true),
363363 prepare_support_default_behavior: None,
364364 honors_change_annotations: Some(false),
365365 }),
@@ -1034,6 +1034,29 @@ impl Client {
10341034 Some(self.call::<lsp::request::DocumentSymbolRequest>(params))
10351035 }
10361036
1037+ pub fn prepare_rename(
1038+ &self,
1039+ text_document: lsp::TextDocumentIdentifier,
1040+ position: lsp::Position,
1041+ ) -> Option<impl Future<Output = Result<Value>>> {
1042+ let capabilities = self.capabilities.get().unwrap();
1043+
1044+ match capabilities.rename_provider {
1045+ Some(lsp::OneOf::Right(lsp::RenameOptions {
1046+ prepare_provider: Some(true),
1047+ ..
1048+ })) => (),
1049+ _ => return None,
1050+ }
1051+
1052+ let params = lsp::TextDocumentPositionParams {
1053+ text_document,
1054+ position,
1055+ };
1056+
1057+ Some(self.call::<lsp::request::PrepareRenameRequest>(params))
1058+ }
1059+
10371060 // empty string to get all symbols
10381061 pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
10391062 let capabilities = self.capabilities.get().unwrap();
helix-term/src/commands/lsp.rs+101−35
@@ -1232,49 +1232,115 @@ pub fn hover(cx: &mut Context) {
12321232}
12331233
12341234pub fn rename_symbol(cx: &mut Context) {
1235− let (view, doc) = current_ref!(cx.editor);
1236− let text = doc.text().slice(..);
1237− let primary_selection = doc.selection(view.id).primary();
1238− let prefill = if primary_selection.len() > 1 {
1239− primary_selection
1240− } else {
1241− use helix_core::textobject::{textobject_word, TextObject};
1242− textobject_word(text, primary_selection, TextObject::Inside, 1, false)
1235+ fn get_prefill_from_word_boundary(editor: &Editor) -> String {
1236+ let (view, doc) = current_ref!(editor);
1237+ let text = doc.text().slice(..);
1238+ let primary_selection = doc.selection(view.id).primary();
1239+ if primary_selection.len() > 1 {
1240+ primary_selection
1241+ } else {
1242+ use helix_core::textobject::{textobject_word, TextObject};
1243+ textobject_word(text, primary_selection, TextObject::Inside, 1, false)
1244+ }
1245+ .fragment(text)
1246+ .into()
12431247 }
1244− .fragment(text)
1245− .into();
1246− ui::prompt_with_input(
1247− cx,
1248− "rename-to:".into(),
1249− prefill,
1250− None,
1251− ui::completers::none,
1252− move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
1253− if event != PromptEvent::Validate {
1254− return;
1248+
1249+ fn get_prefill_from_lsp_response(
1250+ editor: &Editor,
1251+ offset_encoding: OffsetEncoding,
1252+ response: Option<lsp::PrepareRenameResponse>,
1253+ ) -> Result<String, &'static str> {
1254+ match response {
1255+ Some(lsp::PrepareRenameResponse::Range(range)) => {
1256+ let text = doc!(editor).text();
1257+
1258+ Ok(lsp_range_to_range(text, range, offset_encoding)
1259+ .ok_or("lsp sent invalid selection range for rename")?
1260+ .fragment(text.slice(..))
1261+ .into())
1262+ }
1263+ Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { placeholder, .. }) => {
1264+ Ok(placeholder)
1265+ }
1266+ Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => {
1267+ Ok(get_prefill_from_word_boundary(editor))
12551268 }
1269+ None => Err("lsp did not respond to prepare rename request"),
1270+ }
1271+ }
12561272
1257− let (view, doc) = current!(cx.editor);
1258− let language_server = language_server!(cx.editor, doc);
1259− let offset_encoding = language_server.offset_encoding();
1273+ fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
1274+ let prompt = ui::Prompt::new(
1275+ "rename-to:".into(),
1276+ None,
1277+ ui::completers::none,
1278+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
1279+ if event != PromptEvent::Validate {
1280+ return;
1281+ }
12601282
1261− let pos = doc.position(view.id, offset_encoding);
1283+ let (view, doc) = current!(cx.editor);
1284+ let language_server = language_server!(cx.editor, doc);
1285+ let offset_encoding = language_server.offset_encoding();
1286+
1287+ let pos = doc.position(view.id, offset_encoding);
1288+
1289+ let future =
1290+ match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
1291+ Some(future) => future,
1292+ None => {
1293+ cx.editor
1294+ .set_error("Language server does not support symbol renaming");
1295+ return;
1296+ }
1297+ };
1298+ match block_on(future) {
1299+ Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
1300+ Err(err) => cx.editor.set_error(err.to_string()),
1301+ }
1302+ },
1303+ )
1304+ .with_line(prefill, editor);
12621305
1263− let future =
1264− match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
1265− Some(future) => future,
1266− None => {
1267− cx.editor
1268− .set_error("Language server does not support symbol renaming");
1306+ Box::new(prompt)
1307+ }
1308+
1309+ let (view, doc) = current!(cx.editor);
1310+ let language_server = language_server!(cx.editor, doc);
1311+ let offset_encoding = language_server.offset_encoding();
1312+
1313+ let pos = doc.position(view.id, offset_encoding);
1314+
1315+ match language_server.prepare_rename(doc.identifier(), pos) {
1316+ // Language server supports textDocument/prepareRename, use it.
1317+ Some(future) => cx.callback(
1318+ future,
1319+ move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
1320+ let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
1321+ {
1322+ Ok(p) => p,
1323+ Err(e) => {
1324+ editor.set_error(e);
12691325 return;
12701326 }
12711327 };
1272− match block_on(future) {
1273− Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits),
1274− Err(err) => cx.editor.set_error(err.to_string()),
1275− }
1276− },
1277− );
1328+
1329+ let prompt = create_rename_prompt(editor, prefill);
1330+
1331+ compositor.push(prompt);
1332+ },
1333+ ),
1334+ // Language server does not support textDocument/prepareRename, fall back
1335+ // to word boundary selection.
1336+ None => {
1337+ let prefill = get_prefill_from_word_boundary(cx.editor);
1338+
1339+ let prompt = create_rename_prompt(cx.editor, prefill);
1340+
1341+ cx.push_layer(prompt);
1342+ }
1343+ };
12781344}
12791345
12801346pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {