diff options
author | Philipp Mildenberger | 2023-03-20 16:44:04 +0000 |
---|---|---|
committer | Philipp Mildenberger | 2023-05-18 19:58:17 +0000 |
commit | ff262084271492bba239dbc2e5788be3c4d5a4e5 (patch) | |
tree | 0c668cc05e1757377fa3cd418921c9ee575da350 /helix-term/src | |
parent | 9d089c27c77cb2797a0495b46477dfe348d09a91 (diff) |
Filter language servers also by capabilities in `doc.language_servers_with_feature`
* Add `helix_lsp::client::Client::supports_feature(&self, LanguageServerFeature)`
* Extend `doc.language_servers_with_feature` to use this method as filter as well
* Add macro `language_server_with_feature!` to reduce boilerplate for non-mergeable language server requests (like goto-definition)
* Refactored most of the `find_map` code to use the either the macro or filter directly via `doc.language_servers_with_feature`
Diffstat (limited to 'helix-term/src')
-rw-r--r-- | helix-term/src/commands.rs | 145 | ||||
-rw-r--r-- | helix-term/src/commands/lsp.rs | 356 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 9 |
3 files changed, 243 insertions, 267 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d602eaa2..749b0ecf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3236,10 +3236,8 @@ pub mod insert { let trigger_completion = doc .language_servers_with_feature(LanguageServerFeature::Completion) .any(|ls| { - let capabilities = ls.capabilities(); - // TODO: what if trigger is multiple chars long - matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions { + matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions { trigger_characters: Some(triggers), .. }) if triggers.iter().any(|trigger| trigger.contains(ch))) @@ -3252,51 +3250,39 @@ pub mod insert { } fn signature_help(cx: &mut Context, ch: char) { - use futures_util::FutureExt; use helix_lsp::lsp; // if ch matches signature_help char, trigger - let (view, doc) = current!(cx.editor); - // lsp doesn't tell us when to close the signature help, so we request - // the help information again after common close triggers which should - // return None, which in turn closes the popup. - let close_triggers = &[')', ';', '.']; - // TODO support multiple language servers (not just the first that is found) - let future = doc + let doc = doc_mut!(cx.editor); + // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow + let Some(language_server) = doc .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .find_map(|ls| { - let capabilities = ls.capabilities(); - - match capabilities { - lsp::ServerCapabilities { - signature_help_provider: - Some(lsp::SignatureHelpOptions { - trigger_characters: Some(triggers), - // TODO: retrigger_characters - .. - }), - .. - } if triggers.iter().any(|trigger| trigger.contains(ch)) - || close_triggers.contains(&ch) => - { - let pos = doc.position(view.id, ls.offset_encoding()); - ls.text_document_signature_help(doc.identifier(), pos, None) - } - _ if close_triggers.contains(&ch) => ls.text_document_signature_help( - doc.identifier(), - doc.position(view.id, ls.offset_encoding()), - None, - ), - // TODO: what if trigger is multiple chars long - _ => None, - } - }); + .next() + else { + return; + }; - if let Some(future) = future { - super::signature_help_impl_with_future( - cx, - future.boxed(), - SignatureHelpInvoked::Automatic, - ) + let capabilities = language_server.capabilities(); + + if let lsp::ServerCapabilities { + signature_help_provider: + Some(lsp::SignatureHelpOptions { + trigger_characters: Some(triggers), + // TODO: retrigger_characters + .. + }), + .. + } = capabilities + { + // TODO: what if trigger is multiple chars long + let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); + // lsp doesn't tell us when to close the signature help, so we request + // the help information again after common close triggers which should + // return None, which in turn closes the popup. + let close_triggers = &[')', ';', '.']; + + if is_trigger || close_triggers.contains(&ch) { + super::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + } } } @@ -3310,7 +3296,7 @@ pub mod insert { Some(transaction) } - use helix_core::{auto_pairs, syntax::LanguageServerFeature}; + use helix_core::auto_pairs; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current_ref!(cx.editor); @@ -4065,38 +4051,43 @@ fn format_selections(cx: &mut Context) { .set_error("format_selections only supports a single selection for now"); return; } - let future_offset_encoding = doc + + // TODO extra LanguageServerFeature::FormatSelections? + // maybe such that LanguageServerFeature::Format contains it as well + let Some(language_server) = doc .language_servers_with_feature(LanguageServerFeature::Format) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let ranges: Vec<lsp::Range> = doc - .selection(view_id) - .iter() - .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding)) - .collect(); + .find(|ls| { + matches!( + ls.capabilities().document_range_formatting_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ) + }) + else { + cx.editor + .set_error("No configured language server does not support range formatting"); + return; + }; - // TODO: handle fails - // TODO: concurrent map over all ranges + let offset_encoding = language_server.offset_encoding(); + let ranges: Vec<lsp::Range> = doc + .selection(view_id) + .iter() + .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding)) + .collect(); - let range = ranges[0]; + // TODO: handle fails + // TODO: concurrent map over all ranges - let future = language_server.text_document_range_formatting( - doc.identifier(), - range, - lsp::FormattingOptions::default(), - None, - )?; - Some((future, offset_encoding)) - }); + let range = ranges[0]; - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No configured language server supports range formatting"); - return; - } - }; + let future = language_server + .text_document_range_formatting( + doc.identifier(), + range, + lsp::FormattingOptions::default(), + None, + ) + .unwrap(); let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default(); @@ -4247,15 +4238,15 @@ pub fn completion(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::Completion) - // TODO this should probably already been filtered in something like "language_servers_with_feature" .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|language_server| { + .map(|language_server| { let language_server_id = language_server.id(); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); - let completion_request = language_server.completion(doc.identifier(), pos, None)?; + let doc_id = doc.identifier(); + let completion_request = language_server.completion(doc_id, pos, None).unwrap(); - Some(async move { + async move { let json = completion_request.await?; let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?; @@ -4277,7 +4268,7 @@ pub fn completion(cx: &mut Context) { .collect(); anyhow::Ok(items) - }) + } }) .collect(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 6a024bed..15f8d93d 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -45,6 +45,28 @@ use std::{ sync::Arc, }; +/// Gets the first language server that is attached to a document which supports a specific feature. +/// If there is no configured language server that supports the feature, this displays a status message. +/// Using this macro in a context where the editor automatically queries the LSP +/// (instead of when the user explicitly does so via a keybind like `gd`) +/// will spam the "No configured language server supports <feature>" status message confusingly. +#[macro_export] +macro_rules! language_server_with_feature { + ($editor:expr, $doc:expr, $feature:expr) => {{ + let language_server = $doc.language_servers_with_feature($feature).next(); + match language_server { + Some(language_server) => language_server, + None => { + $editor.set_status(format!( + "No configured language server supports {}", + $feature + )); + return; + } + } + }}; +} + impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; @@ -361,36 +383,38 @@ pub fn symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|ls| { - let request = ls.document_symbols(doc.identifier())?; - Some((request, ls.offset_encoding(), doc.identifier())) - }) - .map(|(request, offset_encoding, doc_id)| async move { - let json = request.await?; - let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?; - let symbols = match response { - Some(symbols) => symbols, - None => return anyhow::Ok(vec![]), - }; - // lsp has two ways to represent symbols (flat/nested) - // convert the nested variant to flat, so that we have a homogeneous list - let symbols = match symbols { - lsp::DocumentSymbolResponse::Flat(symbols) => symbols - .into_iter() - .map(|symbol| SymbolInformationItem { - symbol, - offset_encoding, - }) - .collect(), - lsp::DocumentSymbolResponse::Nested(symbols) => { - let mut flat_symbols = Vec::new(); - for symbol in symbols { - nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) + .map(|language_server| { + let request = language_server.document_symbols(doc.identifier()).unwrap(); + let offset_encoding = language_server.offset_encoding(); + let doc_id = doc.identifier(); + + async move { + let json = request.await?; + let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?; + let symbols = match response { + Some(symbols) => symbols, + None => return anyhow::Ok(vec![]), + }; + // lsp has two ways to represent symbols (flat/nested) + // convert the nested variant to flat, so that we have a homogeneous list + let symbols = match symbols { + lsp::DocumentSymbolResponse::Flat(symbols) => symbols + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(), + lsp::DocumentSymbolResponse::Nested(symbols) => { + let mut flat_symbols = Vec::new(); + for symbol in symbols { + nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) + } + flat_symbols } - flat_symbols - } - }; - Ok(symbols) + }; + Ok(symbols) + } }) .collect(); let current_url = doc.url(); @@ -425,20 +449,24 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) - .map(|(request, offset_encoding)| async move { - let json = request.await?; - - let response = serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)? - .unwrap_or_default() - .into_iter() - .map(|symbol| SymbolInformationItem { - symbol, - offset_encoding, - }) - .collect(); - - anyhow::Ok(response) + .map(|language_server| { + let request = language_server.workspace_symbols(pattern.clone()).unwrap(); + let offset_encoding = language_server.offset_encoding(); + async move { + let json = request.await?; + + let response = + serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)? + .unwrap_or_default() + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(); + + anyhow::Ok(response) + } }) .collect(); @@ -1043,22 +1071,19 @@ where F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, { let (view, doc) = current!(cx.editor); - if let Some((future, offset_encoding)) = - doc.run_on_first_supported_language_server(view.id, feature, |ls, encoding, pos, doc_id| { - Some((request_provider(ls, pos, doc_id)?, encoding)) - }) - { - cx.callback( - future, - move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); - } else { - cx.editor - .set_error("No configured language server supports {feature}"); - } + + let language_server = language_server_with_feature!(cx.editor, doc, feature); + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = request_provider(language_server, pos, doc.identifier()).unwrap(); + + cx.callback( + future, + move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { + let items = to_locations(response); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); } pub fn goto_declaration(cx: &mut Context) { @@ -1096,32 +1121,29 @@ pub fn goto_implementation(cx: &mut Context) { pub fn goto_reference(cx: &mut Context) { let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - if let Some((future, offset_encoding)) = doc.run_on_first_supported_language_server( - view.id, - LanguageServerFeature::GotoReference, - |ls, encoding, pos, doc_id| { - Some(( - ls.goto_reference( - doc_id, - pos, - config.lsp.goto_reference_include_declaration, - None, - )?, - encoding, - )) + + // TODO could probably support multiple language servers, + // not sure if there's a real practical use case for this though + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference); + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server + .goto_reference( + doc.identifier(), + pos, + config.lsp.goto_reference_include_declaration, + None, + ) + .unwrap(); + + cx.callback( + future, + move |editor, compositor, response: Option<Vec<lsp::Location>>| { + let items = response.unwrap_or_default(); + goto_impl(editor, compositor, items, offset_encoding); }, - ) { - cx.callback( - future, - move |editor, compositor, response: Option<Vec<lsp::Location>>| { - let items = response.unwrap_or_default(); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); - } else { - cx.editor - .set_error("No configured language server supports goto-reference"); - } + ); } #[derive(PartialEq, Eq, Clone, Copy)] @@ -1145,19 +1167,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { language_server.text_document_signature_help(doc.identifier(), pos, None) }); - let future = match future { - Some(future) => future.boxed(), - None => { - // Do not show the message if signature help was invoked - // automatically on backspace, trigger characters, etc. - if invoked == SignatureHelpInvoked::Manual { - cx.editor - .set_error("No configured language server supports signature-help"); - } - return; + let Some(future) = future else { + // Do not show the message if signature help was invoked + // automatically on backspace, trigger characters, etc. + if invoked == SignatureHelpInvoked::Manual { + cx.editor.set_error("No configured language server supports signature-help"); } + return; }; - signature_help_impl_with_future(cx, future, invoked); + signature_help_impl_with_future(cx, future.boxed(), invoked); } pub fn signature_help_impl_with_future( @@ -1272,22 +1290,14 @@ pub fn signature_help_impl_with_future( pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); + // TODO support multiple language servers (merge UI somehow) + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover); // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier - let request = doc - .language_servers_with_feature(LanguageServerFeature::Hover) - .find_map(|language_server| { - let pos = doc.position(view.id, language_server.offset_encoding()); - language_server.text_document_hover(doc.identifier(), pos, None) - }); - - let future = match request { - Some(future) => future, - None => { - cx.editor - .set_error("No configured language server supports hover"); - return; - } - }; + let pos = doc.position(view.id, language_server.offset_encoding()); + let future = language_server + .text_document_hover(doc.identifier(), pos, None) + .unwrap(); cx.callback( future, @@ -1381,34 +1391,26 @@ pub fn rename_symbol(cx: &mut Context) { return; } let (view, doc) = current!(cx.editor); - let request = doc + + let Some(language_server) = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .find_map(|language_server| { - if let Some(language_server_id) = language_server_id { - if language_server.id() != language_server_id { - return None; - } - } - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.rename_symbol( - doc.identifier(), - pos, - input.to_string(), - )?; - Some((future, offset_encoding)) - }); - - if let Some((future, offset_encoding)) = request { - match block_on(future) { - Ok(edits) => { - let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); - } - Err(err) => cx.editor.set_error(err.to_string()), + .find(|ls| language_server_id.is_none() || Some(ls.id()) == language_server_id) + else { + cx.editor.set_error("No configured language server supports symbol renaming"); + return; + }; + + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server + .rename_symbol(doc.identifier(), pos, input.to_string()) + .unwrap(); + + match block_on(future) { + Ok(edits) => { + let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); } - } else { - cx.editor - .set_error("No configured language server supports symbol renaming"); + Err(err) => cx.editor.set_error(err.to_string()), } }, ) @@ -1417,20 +1419,28 @@ pub fn rename_symbol(cx: &mut Context) { Box::new(prompt) } - let (view, doc) = current!(cx.editor); + let (view, doc) = current_ref!(cx.editor); - let prepare_rename_request = doc + let language_server_with_prepare_rename_support = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.prepare_rename(doc.identifier(), pos)?; - Some((future, offset_encoding, language_server.id())) + .find(|ls| { + matches!( + ls.capabilities().rename_provider, + Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + .. + })) + ) }); - match prepare_rename_request { - // Language server supports textDocument/prepareRename, use it. - Some((future, offset_encoding, ls_id)) => cx.callback( + if let Some(language_server) = language_server_with_prepare_rename_support { + let ls_id = language_server.id(); + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server + .prepare_rename(doc.identifier(), pos) + .unwrap(); + cx.callback( future, move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| { let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response) @@ -1446,38 +1456,23 @@ pub fn rename_symbol(cx: &mut Context) { 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, None); - - cx.push_layer(prompt); - } - }; + ); + } else { + let prefill = get_prefill_from_word_boundary(cx.editor); + let prompt = create_rename_prompt(cx.editor, prefill, None); + cx.push_layer(prompt); + } } pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::DocumentHighlight) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = - language_server.text_document_document_highlight(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No configured language server supports document-highlight"); - return; - } - }; + let language_server = + language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight); + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server + .text_document_document_highlight(doc.identifier(), pos, None) + .unwrap(); cx.callback( future, @@ -1532,16 +1527,9 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let mut language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); - let language_server = language_servers.find(|language_server| { - matches!( - language_server.capabilities().inlay_hint_provider, - Some( - lsp::OneOf::Left(true) - | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)) - ) - ) - })?; + let language_server = doc + .language_servers_with_feature(LanguageServerFeature::InlayHints) + .next()?; let doc_text = doc.text(); let len_lines = doc_text.len_lines(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6f7ed174..ec328ec5 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -394,14 +394,11 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { let matcher = Matcher::default(); - let options = match doc!(editor) + let Some(options) = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) - { - Some(options) => options, - None => { - return vec![]; - } + else { + return vec![]; }; let mut matches: Vec<_> = options |