From 71551d395b4e47804df2d8ecea99e34dbbf16157 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 23 May 2022 18:10:48 +0200 Subject: 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. --- helix-term/src/ui/completion.rs | 85 ++++++++++++++++++++--------------------- helix-term/src/ui/editor.rs | 17 +++------ helix-term/src/ui/mod.rs | 21 +++++----- helix-term/src/ui/statusline.rs | 12 +++--- 4 files changed, 62 insertions(+), 73 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c5c40580..859403a7 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor}; use crate::commands; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; -use helix_lsp::{lsp, util}; +use helix_lsp::{lsp, util, OffsetEncoding}; impl menu::Item for CompletionItem { type Data = (); @@ -38,6 +38,7 @@ impl menu::Item for CompletionItem { || self.item.tags.as_ref().map_or(false, |tags| { tags.contains(&lsp::CompletionItemTag::DEPRECATED) }); + menu::Row::new(vec![ menu::Cell::from(Span::styled( self.item.label.as_str(), @@ -79,19 +80,16 @@ impl menu::Item for CompletionItem { } None => "", }), - // self.detail.as_deref().unwrap_or("") - // self.label_details - // .as_ref() - // .or(self.detail()) - // .as_str(), ]) } } #[derive(Debug, PartialEq, Default, Clone)] -struct CompletionItem { - item: lsp::CompletionItem, - resolved: bool, +pub struct CompletionItem { + pub item: lsp::CompletionItem, + pub language_server_id: usize, + pub offset_encoding: OffsetEncoding, + pub resolved: bool, } /// Wraps a Menu. @@ -109,21 +107,13 @@ impl Completion { pub fn new( editor: &Editor, savepoint: Arc, - mut items: Vec, - offset_encoding: helix_lsp::OffsetEncoding, + mut items: Vec, start_offset: usize, trigger_offset: usize, ) -> Self { let replace_mode = editor.config().completion_replace; // Sort completion items according to their preselect status (given by the LSP server) - items.sort_by_key(|item| !item.preselect.unwrap_or(false)); - let items = items - .into_iter() - .map(|item| CompletionItem { - item, - resolved: false, - }) - .collect(); + items.sort_by_key(|item| !item.item.preselect.unwrap_or(false)); // Then create the menu let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { @@ -131,7 +121,6 @@ impl Completion { doc: &Document, view_id: ViewId, item: &CompletionItem, - offset_encoding: helix_lsp::OffsetEncoding, trigger_offset: usize, include_placeholder: bool, replace_mode: bool, @@ -154,6 +143,8 @@ impl Completion { } }; + let offset_encoding = item.offset_encoding; + let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ return Transaction::new(doc.text()); }; @@ -247,15 +238,8 @@ impl Completion { // always present here let item = item.unwrap(); - let transaction = item_to_transaction( - doc, - view.id, - item, - offset_encoding, - trigger_offset, - true, - replace_mode, - ); + let transaction = + item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode); doc.apply_temporary(&transaction, view.id); } PromptEvent::Validate => { @@ -267,10 +251,15 @@ impl Completion { // always present here let mut item = item.unwrap().clone(); + let language_server = editor + .language_servers + .get_by_id(item.language_server_id) + .unwrap(); + // resolve item if not yet resolved if !item.resolved { if let Some(resolved) = - Self::resolve_completion_item(doc, item.item.clone()) + Self::resolve_completion_item(language_server, item.item.clone()) { item.item = resolved; } @@ -281,7 +270,6 @@ impl Completion { doc, view.id, &item, - offset_encoding, trigger_offset, false, replace_mode, @@ -299,7 +287,7 @@ impl Completion { let transaction = util::generate_transaction_from_edits( doc.text(), additional_edits, - offset_encoding, // TODO: should probably transcode in Client + item.offset_encoding, // TODO: should probably transcode in Client ); doc.apply(&transaction, view.id); } @@ -323,10 +311,17 @@ impl Completion { } fn resolve_completion_item( - doc: &Document, + language_server: &helix_lsp::Client, completion_item: lsp::CompletionItem, ) -> Option { - let language_server = doc.language_server()?; + let completion_resolve_provider = language_server + .capabilities() + .completion_provider + .as_ref()? + .resolve_provider; + if completion_resolve_provider != Some(true) { + return None; + } let future = language_server.resolve_completion_item(completion_item)?; let response = helix_lsp::block_on(future); @@ -397,8 +392,11 @@ impl Completion { Some(item) if !item.resolved => item.clone(), _ => return false, }; - - let language_server = match doc!(cx.editor).language_server() { + let language_server = match cx + .editor + .language_servers + .get_by_id(current_item.language_server_id) + { Some(language_server) => language_server, None => return false, }; @@ -422,13 +420,14 @@ impl Completion { .unwrap() .completion { - completion.replace_item( - current_item, - CompletionItem { - item: resolved_item, - resolved: true, - }, - ); + let resolved_item = CompletionItem { + item: resolved_item, + language_server_id: current_item.language_server_id, + offset_encoding: current_item.offset_encoding, + resolved: true, + }; + + completion.replace_item(current_item, resolved_item); } }, ); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f0989fa8..43b5d1af 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc}; use tui::{buffer::Buffer as Surface, text::Span}; -use super::statusline; +use super::{completion::CompletionItem, statusline}; use super::{document::LineDecoration, lsp::SignatureHelp}; pub struct EditorView { @@ -650,7 +650,7 @@ impl EditorView { .primary() .cursor(doc.text().slice(..)); - let diagnostics = doc.diagnostics().iter().filter(|diagnostic| { + let diagnostics = doc.shown_diagnostics().filter(|diagnostic| { diagnostic.range.start <= cursor && diagnostic.range.end >= cursor }); @@ -953,20 +953,13 @@ impl EditorView { &mut self, editor: &mut Editor, savepoint: Arc, - items: Vec, - offset_encoding: helix_lsp::OffsetEncoding, + items: Vec, start_offset: usize, trigger_offset: usize, size: Rect, ) -> Option { - let mut completion = Completion::new( - editor, - savepoint, - items, - offset_encoding, - start_offset, - trigger_offset, - ); + let mut completion = + Completion::new(editor, savepoint, items, start_offset, trigger_offset); if completion.is_empty() { // skip if we got no completion results diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b0..118836c0 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -17,7 +17,7 @@ mod text; use crate::compositor::{Component, Compositor}; use crate::filter_picker_entry; use crate::job::{self, Callback}; -pub use completion::Completion; +pub use completion::{Completion, CompletionItem}; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; @@ -238,6 +238,7 @@ pub mod completers { use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; + use helix_core::syntax::LanguageServerFeature; use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::theme; use helix_view::{editor::Config, Editor}; @@ -393,17 +394,13 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { let matcher = Matcher::default(); - let (_, doc) = current_ref!(editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => { - return vec![]; - } - }; - - let options = match &language_server.capabilities().execute_command_provider { - Some(options) => options, + let language_servers = + doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); + let options = match language_servers + .into_iter() + .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) + { + Some(id_options) => id_options, None => { return vec![]; } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88786351..b10e8076 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -197,15 +197,16 @@ where ); } +// TODO think about handling multiple language servers fn render_lsp_spinner(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option