diff options
author | Philipp Mildenberger | 2022-05-23 16:10:48 +0000 |
---|---|---|
committer | Philipp Mildenberger | 2023-05-18 19:48:30 +0000 |
commit | 71551d395b4e47804df2d8ecea99e34dbbf16157 (patch) | |
tree | 042b861690af3288ba81b9bad6fa5675255d6858 /helix-term/src/commands.rs | |
parent | 7f5940be80eaa3aec7903903072b7108f41dd97b (diff) |
Adds support for multiple language servers per language.
Language Servers are now configured in a separate table in `languages.toml`:
```toml
[langauge-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
[language-server.efm-lsp-prettier]
command = "efm-langserver"
[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
The language server for a language is configured like this (`typescript-language-server` is configured by default):
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```
or equivalent:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```
Each requested LSP feature is priorized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
If no `except-features` or `only-features` is given all features for the language server are enabled, as long as the language server supports these. If it doesn't the next language server which supports the feature is tried.
The list of supported features are:
- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`
Another side-effect/difference that comes with this PR, is that only one language server instance is started if different languages use the same language server.
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r-- | helix-term/src/commands.rs | 273 |
1 files changed, 154 insertions, 119 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5a844e35..c7d28e19 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -23,6 +23,7 @@ use helix_core::{ regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, selection, shellwords, surround, + syntax::LanguageServerFeature, text_annotations::TextAnnotations, textobject, tree_sitter::Node, @@ -54,13 +55,13 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, - Popup, Prompt, PromptEvent, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, + FilePicker, Picker, Popup, Prompt, PromptEvent, }, }; use crate::job::{self, Jobs}; -use futures_util::StreamExt; +use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt}; use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; @@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) { fn goto_first_diag(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let selection = match doc.diagnostics().first() { + let selection = match doc.shown_diagnostics().next() { Some(diag) => Selection::single(diag.range.start, diag.range.end), None => return, }; @@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let selection = match doc.diagnostics().last() { + let selection = match doc.shown_diagnostics().last() { Some(diag) => Selection::single(diag.range.start, diag.range.end), None => return, }; @@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) { .cursor(doc.text().slice(..)); let diag = doc - .diagnostics() - .iter() + .shown_diagnostics() .find(|diag| diag.range.start > cursor_pos) - .or_else(|| doc.diagnostics().first()); + .or_else(|| doc.shown_diagnostics().next()); let selection = match diag { Some(diag) => Selection::single(diag.range.start, diag.range.end), @@ -3075,11 +3075,12 @@ fn goto_prev_diag(cx: &mut Context) { .cursor(doc.text().slice(..)); let diag = doc - .diagnostics() - .iter() + .shown_diagnostics() + .collect::<Vec<_>>() + .into_iter() .rev() .find(|diag| diag.range.start < cursor_pos) - .or_else(|| doc.diagnostics().last()); + .or_else(|| doc.shown_diagnostics().last()); let selection = match diag { // NOTE: the selection is reversed because we're jumping to the @@ -3234,60 +3235,72 @@ pub mod insert { use helix_lsp::lsp; // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let trigger_completion = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .iter() + .any(|ls| { + let capabilities = ls.capabilities(); - let capabilities = language_server.capabilities(); + // TODO: what if trigger is multiple chars long + matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), + .. + }) if triggers.iter().any(|trigger| trigger.contains(ch))) + }); - if let Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }) = &capabilities.completion_provider - { - // TODO: what if trigger is multiple chars long - if triggers.iter().any(|trigger| trigger.contains(ch)) { - cx.editor.clear_idle_timer(); - super::completion(cx); - } + if trigger_completion { + cx.editor.clear_idle_timer(); + super::completion(cx); } } fn signature_help(cx: &mut Context, ch: char) { + use futures_util::FutureExt; use helix_lsp::lsp; // if ch matches signature_help char, trigger - let doc = doc_mut!(cx.editor); - // The language_server!() macro is not used here since it will - // print an "LSP not active for current buffer" message on - // every keypress. - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let capabilities = language_server.capabilities(); + 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 + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .iter() + .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, + } + }); - 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); - } + if let Some(future) = future { + super::signature_help_impl_with_future( + cx, + future.boxed(), + SignatureHelpInvoked::Automatic, + ) } } @@ -3301,7 +3314,7 @@ pub mod insert { Some(transaction) } - use helix_core::auto_pairs; + use helix_core::{auto_pairs, syntax::LanguageServerFeature}; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current_ref!(cx.editor); @@ -4046,55 +4059,55 @@ fn format_selections(cx: &mut Context) { use helix_lsp::{lsp, util::range_to_lsp_range}; let (view, doc) = current!(cx.editor); + let view_id = view.id; // via lsp if available // TODO: else via tree-sitter indentation calculations - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let ranges: Vec<lsp::Range> = doc - .selection(view.id) - .iter() - .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding())) - .collect(); - - if ranges.len() != 1 { + if doc.selection(view_id).len() != 1 { cx.editor .set_error("format_selections only supports a single selection for now"); return; } - // TODO: handle fails - // TODO: concurrent map over all ranges - - let range = ranges[0]; - - let request = match language_server.text_document_range_formatting( - doc.identifier(), - range, - lsp::FormattingOptions::default(), - None, - ) { - Some(future) => future, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::Format) + .iter() + .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(); + + // TODO: handle fails + // TODO: concurrent map over all ranges + + let range = ranges[0]; + + let future = language_server.text_document_range_formatting( + doc.identifier(), + range, + lsp::FormattingOptions::default(), + None, + )?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support range formatting"); + .set_error("No language server supports range formatting"); return; } }; - let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default(); + let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default(); - let transaction = helix_lsp::util::generate_transaction_from_edits( - doc.text(), - edits, - language_server.offset_encoding(), - ); + let transaction = + helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding); - doc.apply(&transaction, view.id); + doc.apply(&transaction, view_id); } fn join_selections_impl(cx: &mut Context, select_space: bool) { @@ -4231,21 +4244,45 @@ pub fn completion(cx: &mut Context) { doc.savepoint(view) }; - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let offset_encoding = language_server.offset_encoding(); let text = savepoint.text.clone(); let cursor = savepoint.cursor(); - let pos = pos_to_lsp_pos(&text, cursor, offset_encoding); + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .iter() + // TODO this should probably already been filtered in something like "language_servers_with_feature" + .filter_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, helix_lsp::OffsetEncoding::Utf8); + let completion_request = language_server.completion(doc.identifier(), pos, None)?; + + Some(async move { + let json = completion_request.await?; + let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?; + + let items = match response { + Some(lsp::CompletionResponse::Array(items)) => items, + // TODO: do something with is_incomplete + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: _is_incomplete, + items, + })) => items, + None => Vec::new(), + } + .into_iter() + .map(|item| CompletionItem { + item, + language_server_id, + offset_encoding, + resolved: false, + }) + .collect(); - let future = match language_server.completion(doc.identifier(), pos, None) { - Some(future) => future, - None => return, - }; + anyhow::Ok(items) + }) + }) + .collect(); // setup a channel that allows the request to be canceled let (tx, rx) = oneshot::channel(); @@ -4254,12 +4291,20 @@ pub fn completion(cx: &mut Context) { // and the associated request is automatically dropped cx.editor.completion_request_handle = Some(tx); let future = async move { + let items_future = async move { + let mut items = Vec::new(); + // TODO if one completion request errors, all other completion requests are discarded (even if they're valid) + while let Some(mut lsp_items) = futures.try_next().await? { + items.append(&mut lsp_items); + } + anyhow::Ok(items) + }; tokio::select! { biased; _ = rx => { - Ok(serde_json::Value::Null) + Ok(Vec::new()) } - res = future => { + res = items_future => { res } } @@ -4293,9 +4338,9 @@ pub fn completion(cx: &mut Context) { }, )); - cx.callback( - future, - move |editor, compositor, response: Option<lsp::CompletionResponse>| { + cx.jobs.callback(async move { + let items = future.await?; + let call = move |editor: &mut Editor, compositor: &mut Compositor| { let (view, doc) = current_ref!(editor); // check if the completion request is stale. // @@ -4306,16 +4351,6 @@ pub fn completion(cx: &mut Context) { return; } - let items = match response { - Some(lsp::CompletionResponse::Array(items)) => items, - // TODO: do something with is_incomplete - Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: _is_incomplete, - items, - })) => items, - None => Vec::new(), - }; - if items.is_empty() { // editor.set_error("No completion available"); return; @@ -4326,7 +4361,6 @@ pub fn completion(cx: &mut Context) { editor, savepoint, items, - offset_encoding, start_offset, trigger_offset, size, @@ -4340,8 +4374,9 @@ pub fn completion(cx: &mut Context) { { compositor.remove(SignatureHelp::ID); } - }, - ); + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }); } // comments @@ -5141,7 +5176,7 @@ async fn shell_impl_async( helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input) .await?; } - Ok::<_, anyhow::Error>(()) + anyhow::Ok(()) }); let (output, _) = tokio::join! { process.wait_with_output(), |