Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSP: Support textDocument/prepareRename #6103

Merged
merged 8 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}),
Expand Down Expand Up @@ -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();
Expand Down
136 changes: 101 additions & 35 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +1323 to +1324
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we return early here we won't be able to try the rename. That might be ok: if get_prefill_from_lsp_response returns an Err, the language server either doesn't support renaming at the current position or it sent an invalid range which is a problem too.

Alternatively though we could fall back to the word textobject prefill value here. I'm not sure which is better behavior. Let's leave it as-is for now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the time to investigate myself right now but it might be worth looking at what vscode does here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find exactly where this is handled in vscode but it looks like the current behavior here matches nvim's https://github.com/neovim/neovim/blob/ca3bc56a3b1b3454498a4a23c0d700486d554077/runtime/lua/vim/lsp/buf.lua#L319-L379

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) {
Expand Down