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-view/src/document.rs | 145 ++++++++++++++++++++++++++++++++------------- helix-view/src/editor.rs | 64 +++++++++++--------- helix-view/src/gutter.rs | 2 +- 3 files changed, 141 insertions(+), 70 deletions(-) (limited to 'helix-view') diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index eb376567..734d76d1 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -6,7 +6,7 @@ use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; -use helix_core::syntax::Highlight; +use helix_core::syntax::{Highlight, LanguageServerFeature, LanguageServerFeatureConfiguration}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -16,7 +16,7 @@ use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; use std::cell::Cell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; @@ -180,7 +180,7 @@ pub struct Document { pub(crate) modified_since_accessed: bool, diagnostics: Vec, - language_server: Option>, + language_servers: Vec>, diff_handle: Option, version_control_head: Option>>>, @@ -616,7 +616,7 @@ impl Document { last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, - language_server: None, + language_servers: Vec::new(), diff_handle: None, config, version_control_head: None, @@ -730,19 +730,24 @@ impl Document { return Some(formatting_future.boxed()); }; - let language_server = self.language_server()?; let text = self.text.clone(); - let offset_encoding = language_server.offset_encoding(); - - let request = language_server.text_document_formatting( - self.identifier(), - lsp::FormattingOptions { - tab_size: self.tab_width() as u32, - insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), - ..Default::default() - }, - None, - )?; + // finds first language server that supports formatting and then formats + let (offset_encoding, request) = self + .language_servers_with_feature(LanguageServerFeature::Format) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions { + tab_size: self.tab_width() as u32, + insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), + ..Default::default() + }, + None, + )?; + Some((offset_encoding, request)) + })?; let fut = async move { let edits = request.await.unwrap_or_else(|e| { @@ -797,13 +802,12 @@ impl Document { if self.path.is_none() { bail!("Can't save with no path set!"); } - self.path.as_ref().unwrap().clone() } }; let identifier = self.path().map(|_| self.identifier()); - let language_server = self.language_server.clone(); + let language_servers = self.language_servers.clone(); // mark changes up to now as saved let current_rev = self.get_current_revision(); @@ -847,14 +851,13 @@ impl Document { text: text.clone(), }; - if let Some(language_server) = language_server { + for language_server in language_servers { if !language_server.is_initialized() { return Ok(event); } - - if let Some(identifier) = identifier { + if let Some(identifier) = &identifier { if let Some(notification) = - language_server.text_document_did_save(identifier, &text) + language_server.text_document_did_save(identifier.clone(), &text) { notification.await?; } @@ -1005,8 +1008,8 @@ impl Document { } /// Set the LSP. - pub fn set_language_server(&mut self, language_server: Option>) { - self.language_server = language_server; + pub fn set_language_servers(&mut self, language_servers: Vec>) { + self.language_servers = language_servers; } /// Select text within the [`Document`]. @@ -1159,7 +1162,7 @@ impl Document { if emit_lsp_notification { // emit lsp notification - if let Some(language_server) = self.language_server() { + for language_server in self.language_servers() { let notify = language_server.text_document_did_change( self.versioned_identifier(), &old_doc, @@ -1415,18 +1418,13 @@ impl Document { .map(|language| language.language_id.as_str()) } - /// Language ID for the document. Either the `language-id` from the - /// `language-server` configuration, or the document language if no - /// `language-id` has been specified. + /// Language ID for the document. Either the `language-id`, + /// or the document language name if no `language-id` has been specified. pub fn language_id(&self) -> Option<&str> { - let language_config = self.language.as_deref()?; - - language_config - .language_server - .as_ref()? - .language_id + self.language_config()? + .language_server_language_id .as_deref() - .or(Some(language_config.language_id.as_str())) + .or_else(|| self.language_name()) } /// Corresponding [`LanguageConfiguration`]. @@ -1439,10 +1437,54 @@ impl Document { self.version } - /// Language server if it has been initialized. - pub fn language_server(&self) -> Option<&helix_lsp::Client> { - let server = self.language_server.as_deref()?; - server.is_initialized().then_some(server) + /// Language servers that have been initialized. + pub fn language_servers(&self) -> Vec<&helix_lsp::Client> { + self.language_servers + .iter() + .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None }) + .collect() + } + + // TODO filter also based on LSP capabilities? + pub fn language_servers_with_feature( + &self, + feature: LanguageServerFeature, + ) -> Vec<&helix_lsp::Client> { + let language_servers = self.language_servers(); + + let language_config = match self.language_config() { + Some(language_config) => language_config, + None => return Vec::new(), + }; + + // O(n^2) but since language_servers will be of very small length, + // I don't see the necessity to optimize + language_config + .language_servers + .iter() + .filter_map(|c| match c { + LanguageServerFeatureConfiguration::Simple(name) => language_servers + .iter() + .find(|ls| ls.name() == name) + .copied(), + LanguageServerFeatureConfiguration::Features { + only_features, + except_features, + name, + } => { + if (only_features.is_empty() || only_features.contains(&feature)) + && !except_features.contains(&feature) + { + language_servers + .iter() + .find(|ls| ls.name() == name) + .copied() + } else { + None + } + } + }) + .collect() } pub fn diff_handle(&self) -> Option<&DiffHandle> { @@ -1565,12 +1607,33 @@ impl Document { &self.diagnostics } - pub fn set_diagnostics(&mut self, diagnostics: Vec) { - self.diagnostics = diagnostics; + pub fn shown_diagnostics(&self) -> impl Iterator { + let ls_ids: HashSet<_> = self + .language_servers_with_feature(LanguageServerFeature::Diagnostics) + .iter() + .map(|ls| ls.id()) + .collect(); + self.diagnostics + .iter() + .filter(move |d| ls_ids.contains(&d.language_server_id)) + } + + pub fn replace_diagnostics( + &mut self, + mut diagnostics: Vec, + language_server_id: usize, + ) { + self.clear_diagnostics(language_server_id); + self.diagnostics.append(&mut diagnostics); self.diagnostics .sort_unstable_by_key(|diagnostic| diagnostic.range); } + pub fn clear_diagnostics(&mut self, language_server_id: usize) { + self.diagnostics + .retain(|d| d.language_server_id != language_server_id); + } + /// Get the document's auto pairs. If the document has a recognized /// language config with auto pairs configured, returns that; /// otherwise, falls back to the global auto pairs config. If the global diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9546d460..5ca9aceb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -48,7 +48,7 @@ use helix_core::{ }; use helix_core::{Position, Selection}; use helix_dap as dap; -use helix_lsp::lsp; +use helix_lsp::{lsp, OffsetEncoding}; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE @@ -818,7 +818,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap>, + pub diagnostics: BTreeMap>, pub diff_providers: DiffProviderRegistry, pub debugger: Option, @@ -941,6 +941,7 @@ impl Editor { syn_loader: Arc, config: Arc>, ) -> Self { + let language_servers = helix_lsp::Registry::new(syn_loader.clone()); let conf = config.load(); let auto_pairs = (&conf.auto_pairs).into(); @@ -960,7 +961,7 @@ impl Editor { macro_recording: None, macro_replaying: Vec::new(), theme: theme_loader.default(), - language_servers: helix_lsp::Registry::new(), + language_servers, diagnostics: BTreeMap::new(), diff_providers: DiffProviderRegistry::default(), debugger: None, @@ -1093,12 +1094,12 @@ impl Editor { } /// Refreshes the language server for a given document - pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { - self.launch_language_server(doc_id) + pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> { + self.launch_language_servers(doc_id) } /// Launch a language server for a given document - fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> { + fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> { if !self.config().lsp.enable { return None; } @@ -1109,42 +1110,49 @@ impl Editor { let config = doc.config.load(); let root_dirs = &config.workspace_lsp_roots; - // try to find a language server based on the language name - let language_server = lang.as_ref().and_then(|language| { + // try to find language servers based on the language name + let language_servers = lang.as_ref().and_then(|language| { self.language_servers .get(language, path.as_ref(), root_dirs, config.lsp.snippets) .map_err(|e| { log::error!( - "Failed to initialize the LSP for `{}` {{ {} }}", + "Failed to initialize the language servers for `{}` {{ {} }}", language.scope(), e ) }) .ok() - .flatten() }); let doc = self.document_mut(doc_id)?; let doc_url = doc.url()?; - if let Some(language_server) = language_server { - // only spawn a new lang server if the servers aren't the same - if Some(language_server.id()) != doc.language_server().map(|server| server.id()) { - if let Some(language_server) = doc.language_server() { - tokio::spawn(language_server.text_document_did_close(doc.identifier())); + if let Some(language_servers) = language_servers { + // only spawn new lang servers if the servers aren't the same + let doc_language_servers = doc.language_servers(); + let spawn_new_servers = language_servers.len() != doc_language_servers.len() + || language_servers + .iter() + .zip(doc_language_servers.iter()) + .any(|(l, dl)| l.id() != dl.id()); + if spawn_new_servers { + for doc_language_server in doc_language_servers { + tokio::spawn(doc_language_server.text_document_did_close(doc.identifier())); } let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); - // TODO: this now races with on_init code if the init happens too quickly - tokio::spawn(language_server.text_document_did_open( - doc_url, - doc.version(), - doc.text(), - language_id, - )); + for language_server in &language_servers { + // TODO: this now races with on_init code if the init happens too quickly + tokio::spawn(language_server.text_document_did_open( + doc_url.clone(), + doc.version(), + doc.text(), + language_id.clone(), + )); + } - doc.set_language_server(Some(language_server)); + doc.set_language_servers(language_servers); } } Some(()) @@ -1337,10 +1345,10 @@ impl Editor { } doc.set_version_control_head(self.diff_providers.get_current_head_name(&path)); - let id = self.new_document(doc); - let _ = self.launch_language_server(id); + let doc_id = self.new_document(doc); + let _ = self.launch_language_servers(doc_id); - id + doc_id }; self.switch(id, action); @@ -1368,7 +1376,7 @@ impl Editor { // This will also disallow any follow-up writes self.saves.remove(&doc_id); - if let Some(language_server) = doc.language_server() { + for language_server in doc.language_servers() { // TODO: track error tokio::spawn(language_server.text_document_did_close(doc.identifier())); } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 3ecae919..78f879c9 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -55,7 +55,7 @@ pub fn diagnostic<'doc>( let error = theme.get("error"); let info = theme.get("info"); let hint = theme.get("hint"); - let diagnostics = doc.diagnostics(); + let diagnostics = doc.shown_diagnostics().collect::>(); Box::new( move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { -- cgit v1.2.3-70-g09d2 From 74e21e1b250be884242596cbd7d98b098bb0fd0c Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 13 Mar 2023 15:01:11 +0100 Subject: Fix some lints/docgen hints --- book/src/generated/typable-cmd.md | 2 +- helix-term/src/health.rs | 5 +---- helix-view/src/editor.rs | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) (limited to 'helix-view') diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 0c377b3b..cc2e87ea 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -50,7 +50,7 @@ | `:reload-all` | Discard changes and reload all documents from the source files. | | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | -| `:lsp-restart` | Restarts the language servers used by the currently opened file | +| `:lsp-restart` | Restarts the language servers used by the current doc | | `:lsp-stop` | Stops the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 031f982c..6b9f8517 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -2,10 +2,7 @@ use crossterm::{ style::{Color, Print, Stylize}, tty::IsTty, }; -use helix_core::{ - config::{default_syntax_loader, user_syntax_loader}, - syntax::LanguageServerFeatureConfiguration, -}; +use helix_core::config::{default_syntax_loader, user_syntax_loader}; use helix_loader::grammar::load_runtime_file; use helix_view::clipboard::get_clipboard_provider; use std::io::Write; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5ca9aceb..697d4459 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE -- cgit v1.2.3-70-g09d2 From f9b08656f41cbb9573ffb144f5dc2e24ea764ac9 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Fri, 17 Mar 2023 15:30:49 +0100 Subject: Fix sorting issues of the editor wide diagnostics and apply diagnostics related review suggestions Co-authored-by: Pascal Kuthe --- helix-term/src/application.rs | 21 +++++++++++---------- helix-term/src/commands/lsp.rs | 17 ++++++++++------- helix-term/src/ui/statusline.rs | 2 +- helix-view/src/editor.rs | 2 +- 4 files changed, 23 insertions(+), 19 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index e159cb83..728aa46a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -721,7 +721,7 @@ impl Application { )); } } - Notification::PublishDiagnostics(mut params) => { + Notification::PublishDiagnostics(params) => { let path = match params.uri.to_file_path() { Ok(path) => path, Err(_) => { @@ -841,15 +841,10 @@ impl Application { doc.replace_diagnostics(diagnostics, server_id); } - // Sort diagnostics first by severity and then by line numbers. - // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order - params - .diagnostics - .sort_unstable_by_key(|d| (d.severity, d.range.start)); - let diagnostics = params + let mut diagnostics = params .diagnostics .into_iter() - .map(|d| (d, server_id, offset_encoding)) + .map(|d| (d, server_id)) .collect(); // Insert the original lsp::Diagnostics here because we may have no open document @@ -859,10 +854,16 @@ impl Application { Entry::Occupied(o) => { let current_diagnostics = o.into_mut(); // there may entries of other language servers, which is why we can't overwrite the whole entry - current_diagnostics.retain(|(_, lsp_id, _)| *lsp_id != server_id); - current_diagnostics.extend(diagnostics); + current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id); + current_diagnostics.append(&mut diagnostics); + // Sort diagnostics first by severity and then by line numbers. + // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order + current_diagnostics + .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); } Entry::Vacant(v) => { + diagnostics + .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start)); v.insert(diagnostics); } }; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index efef1211..1a1233a9 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -262,7 +262,7 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap>, + diagnostics: BTreeMap>, current_path: Option, format: DiagnosticsFormat, ) -> FilePicker { @@ -272,12 +272,15 @@ fn diag_picker( let mut flat_diag = Vec::new(); for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); - for (diag, _, offset_encoding) in diags { - flat_diag.push(PickerDiagnostic { - url: url.clone(), - diag, - offset_encoding, - }); + + for (diag, ls) in diags { + if let Some(ls) = cx.editor.language_servers.get_by_id(ls) { + flat_diag.push(PickerDiagnostic { + url: url.clone(), + diag, + offset_encoding: ls.offset_encoding(), + }); + } } } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index b10e8076..60997956 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -266,7 +266,7 @@ where .diagnostics .values() .flatten() - .fold((0, 0), |mut counts, (diag, _, _)| { + .fold((0, 0), |mut counts, (diag, _)| { match diag.severity { Some(DiagnosticSeverity::WARNING) => counts.0 += 1, Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 697d4459..2bd48af8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -818,7 +818,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap>, + pub diagnostics: BTreeMap>, pub diff_providers: DiffProviderRegistry, pub debugger: Option, -- cgit v1.2.3-70-g09d2 From dd2f74794a2ba6c45693d218079349d27828caec Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 18 Mar 2023 16:45:07 +0100 Subject: Fix error messages when no language server is available Co-authored-by: Skyler Hawthorne --- helix-term/src/commands/lsp.rs | 4 ++-- helix-view/src/editor.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 1a1233a9..25a54aba 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1130,7 +1130,7 @@ pub fn goto_implementation(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("no language server supports goto-implementation"); + .set_error("No language server supports goto-implementation"); return; } }; @@ -1164,7 +1164,7 @@ pub fn goto_reference(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("language server supports goto-reference"); + .set_error("No language server supports goto-reference"); return; } }; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 2bd48af8..366cc01c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -48,7 +48,7 @@ use helix_core::{ }; use helix_core::{Position, Selection}; use helix_dap as dap; -use helix_lsp::{lsp, OffsetEncoding}; +use helix_lsp::lsp; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; -- cgit v1.2.3-70-g09d2 From 0637691eb1fb7e2055fc04a0209be94906c2bd1a Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 18 Mar 2023 17:00:57 +0100 Subject: Use DoubleEndedIterator instead of collect to Vec for reversing Co-authored-by: Pascal Kuthe --- helix-term/src/commands.rs | 2 -- helix-view/src/document.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 27f289f0..14a68490 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3076,8 +3076,6 @@ fn goto_prev_diag(cx: &mut Context) { let diag = doc .shown_diagnostics() - .collect::>() - .into_iter() .rev() .find(|diag| diag.range.start < cursor_pos) .or_else(|| doc.shown_diagnostics().last()); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 734d76d1..37ddc2b6 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1607,7 +1607,7 @@ impl Document { &self.diagnostics } - pub fn shown_diagnostics(&self) -> impl Iterator { + pub fn shown_diagnostics(&self) -> impl Iterator + DoubleEndedIterator { let ls_ids: HashSet<_> = self .language_servers_with_feature(LanguageServerFeature::Diagnostics) .iter() -- cgit v1.2.3-70-g09d2 From 76b5cab52479daf25ffa0af798c1ebcf6a4f0004 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 18 Mar 2023 20:12:20 +0100 Subject: Refactored doc.language_servers and doc.language_servers_with_feature to return an iterator and refactor LanguageServerFeature handling to a HashMap (language server name maps to features) Co-authored-by: Pascal Kuthe --- helix-core/src/syntax.rs | 97 ++++++++++++++++++++++++++++++++-------- helix-lsp/src/lib.rs | 20 ++++----- helix-term/src/application.rs | 8 ++-- helix-term/src/commands.rs | 11 ++--- helix-term/src/commands/lsp.rs | 55 +++++++++++------------ helix-term/src/commands/typed.rs | 24 +++++----- helix-term/src/health.rs | 8 ++-- helix-term/src/ui/mod.rs | 8 ++-- helix-term/src/ui/statusline.rs | 5 +-- helix-view/src/document.rs | 52 +++++---------------- helix-view/src/editor.rs | 5 ++- 11 files changed, 155 insertions(+), 138 deletions(-) (limited to 'helix-view') diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f45a38cc..a4e6d990 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -16,7 +16,7 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap}; use std::{ borrow::Cow, cell::RefCell, - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, fmt::{self, Display}, hash::{Hash, Hasher}, mem::{replace, transmute}, @@ -26,7 +26,7 @@ use std::{ }; use once_cell::sync::{Lazy, OnceCell}; -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeSeq, Deserialize, Serialize}; use helix_loader::grammar::{get_language, load_runtime_file}; @@ -110,8 +110,13 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub language_servers: Vec, + #[serde( + default, + skip_serializing_if = "HashMap::is_empty", + serialize_with = "serialize_lang_features", + deserialize_with = "deserialize_lang_features" + )] + pub language_servers: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option, @@ -211,7 +216,7 @@ impl<'de> Deserialize<'de> for FileType { } } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] pub enum LanguageServerFeature { Format, @@ -261,18 +266,81 @@ impl Display for LanguageServerFeature { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] -pub enum LanguageServerFeatureConfiguration { +enum LanguageServerFeatureConfiguration { #[serde(rename_all = "kebab-case")] Features { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - only_features: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - except_features: Vec, + #[serde(default, skip_serializing_if = "HashSet::is_empty")] + only_features: HashSet, + #[serde(default, skip_serializing_if = "HashSet::is_empty")] + except_features: HashSet, name: String, }, Simple(String), } +#[derive(Debug, Default)] +pub struct LanguageServerFeatures { + pub only: HashSet, + pub excluded: HashSet, +} + +impl LanguageServerFeatures { + pub fn has_feature(&self, feature: LanguageServerFeature) -> bool { + self.only.is_empty() || self.only.contains(&feature) && !self.excluded.contains(&feature) + } +} + +fn deserialize_lang_features<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw: Vec = Deserialize::deserialize(deserializer)?; + let res = raw + .into_iter() + .map(|config| match config { + LanguageServerFeatureConfiguration::Simple(name) => { + (name, LanguageServerFeatures::default()) + } + LanguageServerFeatureConfiguration::Features { + only_features, + except_features, + name, + } => ( + name, + LanguageServerFeatures { + only: only_features, + excluded: except_features, + }, + ), + }) + .collect(); + Ok(res) +} +fn serialize_lang_features( + map: &HashMap, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + let mut serializer = serializer.serialize_seq(Some(map.len()))?; + for (name, features) in map { + let features = if features.only.is_empty() && features.excluded.is_empty() { + LanguageServerFeatureConfiguration::Simple(name.to_owned()) + } else { + LanguageServerFeatureConfiguration::Features { + only_features: features.only.clone(), + except_features: features.excluded.clone(), + name: name.to_owned(), + } + }; + serializer.serialize_element(&features)?; + } + serializer.end() +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -650,15 +718,6 @@ pub struct SoftWrap { pub wrap_at_text_width: Option, } -impl LanguageServerFeatureConfiguration { - pub fn name(&self) -> &String { - match self { - LanguageServerFeatureConfiguration::Simple(name) => name, - LanguageServerFeatureConfiguration::Features { name, .. } => name, - } - } -} - // Expose loader as Lazy<> global since it's always static? #[derive(Debug)] diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 12e63255..ba0c3fee 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -689,12 +689,10 @@ impl Registry { ) -> Result>> { language_config .language_servers - .iter() - .filter_map(|config| { - let name = config.name().clone(); - + .keys() + .filter_map(|name| { #[allow(clippy::map_entry)] - if self.inner.contains_key(&name) { + if self.inner.contains_key(name) { let client = match self.start_client( name.clone(), language_config, @@ -705,7 +703,10 @@ impl Registry { Ok(client) => client, error => return Some(error), }; - let old_clients = self.inner.insert(name, vec![client.clone()]).unwrap(); + let old_clients = self + .inner + .insert(name.clone(), vec![client.clone()]) + .unwrap(); // TODO what if there are different language servers for different workspaces, // I think the language servers will be stopped without being restarted, which is not intended @@ -742,9 +743,8 @@ impl Registry { ) -> Result>> { language_config .language_servers - .iter() - .map(|features| { - let name = features.name(); + .keys() + .map(|name| { if let Some(clients) = self.inner.get_mut(name) { if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) @@ -759,7 +759,7 @@ impl Registry { root_dirs, enable_snippets, )?; - let clients = self.inner.entry(features.name().clone()).or_default(); + let clients = self.inner.entry(name.clone()).or_default(); clients.push(client.clone()); Ok(client) }) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 728aa46a..83473179 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -699,9 +699,10 @@ impl Application { tokio::spawn(language_server.did_change_configuration(config.clone())); } - let docs = self.editor.documents().filter(|doc| { - doc.language_servers().iter().any(|l| l.id() == server_id) - }); + let docs = self + .editor + .documents() + .filter(|doc| doc.language_servers().any(|l| l.id() == server_id)); // trigger textDocument/didOpen for docs that are already open for doc in docs { @@ -970,7 +971,6 @@ impl Application { .filter_map(|doc| { if doc .language_servers() - .iter() .any(|server| server.id() == server_id) { doc.clear_diagnostics(server_id); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 14a68490..060c9d83 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3235,7 +3235,6 @@ pub mod insert { let doc = doc_mut!(cx.editor); let trigger_completion = doc .language_servers_with_feature(LanguageServerFeature::Completion) - .iter() .any(|ls| { let capabilities = ls.capabilities(); @@ -3264,7 +3263,6 @@ pub mod insert { // 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(); @@ -4067,10 +4065,8 @@ fn format_selections(cx: &mut Context) { .set_error("format_selections only supports a single selection for now"); return; } - - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::Format) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let ranges: Vec = doc @@ -4091,7 +4087,9 @@ fn format_selections(cx: &mut Context) { None, )?; Some((future, offset_encoding)) - }) { + }); + + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -4247,7 +4245,6 @@ pub fn completion(cx: &mut Context) { 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(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 25a54aba..6553ce16 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -353,7 +353,6 @@ pub fn symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) - .iter() .filter_map(|ls| { let request = ls.document_symbols(doc.identifier())?; Some((request, ls.offset_encoding(), doc.identifier())) @@ -420,7 +419,6 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let doc = doc!(editor); let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) - .iter() .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) .map(|(request, offset_encoding)| async move { let json = request.await?; @@ -581,7 +579,6 @@ pub fn code_action(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::CodeAction) - .iter() // TODO this should probably already been filtered in something like "language_servers_with_feature" .filter_map(|language_server| { let offset_encoding = language_server.offset_encoding(); @@ -1034,15 +1031,15 @@ fn to_locations(definitions: Option) -> Vec future_offset_encoding, None => { cx.editor @@ -1062,15 +1059,15 @@ pub fn goto_declaration(cx: &mut Context) { pub fn goto_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoDefinition) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_definition(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 @@ -1090,15 +1087,15 @@ pub fn goto_definition(cx: &mut Context) { pub fn goto_type_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_type_definition(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 @@ -1118,15 +1115,15 @@ pub fn goto_type_definition(cx: &mut Context) { pub fn goto_implementation(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoImplementation) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_implementation(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 @@ -1147,9 +1144,8 @@ 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); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoReference) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); @@ -1160,7 +1156,8 @@ pub fn goto_reference(cx: &mut Context) { None, )?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1192,13 +1189,14 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { let (view, doc) = current!(cx.editor); // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it - let future = match doc + let future = doc .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .iter() .find_map(|language_server| { let pos = doc.position(view.id, language_server.offset_encoding()); 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 @@ -1328,7 +1326,6 @@ pub fn hover(cx: &mut Context) { // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier let request = doc .language_servers_with_feature(LanguageServerFeature::Hover) - .iter() .find_map(|language_server| { let pos = doc.position(view.id, language_server.offset_encoding()); language_server.text_document_hover(doc.identifier(), pos, None) @@ -1436,7 +1433,6 @@ pub fn rename_symbol(cx: &mut Context) { let (view, doc) = current!(cx.editor); let request = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .iter() .find_map(|language_server| { if let Some(language_server_id) = language_server_id { if language_server.id() != language_server_id { @@ -1475,7 +1471,6 @@ pub fn rename_symbol(cx: &mut Context) { let prepare_rename_request = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); @@ -1516,17 +1511,17 @@ pub fn rename_symbol(cx: &mut Context) { pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::DocumentHighlight) - .iter() .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)) - }) { - Some(future) => future, + }); + let (future, offset_encoding) = match future_offset_encoding { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor .set_error("No language server supports document-highlight"); @@ -1587,8 +1582,8 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); - let language_server = language_servers.iter().find(|language_server| { + 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( diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index b78de772..38058ed5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1330,14 +1330,16 @@ fn lsp_workspace_command( return Ok(()); } let doc = doc!(cx.editor); - let language_servers = - doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); - let (language_server_id, options) = match language_servers.iter().find_map(|ls| { - ls.capabilities() - .execute_command_provider - .as_ref() - .map(|options| (ls.id(), options)) - }) { + let id_options = doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) + .find_map(|ls| { + ls.capabilities() + .execute_command_provider + .as_ref() + .map(|options| (ls.id(), options)) + }); + + let (language_server_id, options) = match id_options { Some(id_options) => id_options, None => { cx.editor.set_status( @@ -1346,6 +1348,7 @@ fn lsp_workspace_command( return Ok(()); } }; + if args.is_empty() { let commands = options .commands @@ -1445,7 +1448,6 @@ fn lsp_stop( // I'm not sure if this is really what we want let ls_shutdown_names = doc .language_servers() - .iter() .map(|ls| ls.name()) .collect::>(); @@ -1459,7 +1461,6 @@ fn lsp_stop( .filter_map(|doc| { let doc_active_ls_ids: Vec<_> = doc .language_servers() - .iter() .filter(|ls| !ls_shutdown_names.contains(&ls.name())) .map(|ls| ls.id()) .collect(); @@ -1472,7 +1473,7 @@ fn lsp_stop( .map(Clone::clone) .collect(); - if active_clients.len() != doc.language_servers().len() { + if active_clients.len() != doc.language_servers().count() { Some((doc.id(), active_clients)) } else { None @@ -1485,7 +1486,6 @@ fn lsp_stop( let stopped_clients: Vec<_> = doc .language_servers() - .iter() .filter(|ls| { !active_clients .iter() diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6b9f8517..5b22ea55 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -194,10 +194,10 @@ pub fn languages_all() -> std::io::Result<()> { // TODO multiple language servers (check binary for each supported language server, not just the first) - let lsp = lang.language_servers.first().and_then(|lsp| { + let lsp = lang.language_servers.keys().next().and_then(|ls_name| { syn_loader_conf .language_server - .get(lsp.name()) + .get(ls_name) .map(|config| config.command.clone()) }); check_binary(lsp); @@ -271,10 +271,10 @@ pub fn language(lang_str: String) -> std::io::Result<()> { // TODO multiple language servers probe_protocol( "language server", - lang.language_servers.first().and_then(|lsp| { + lang.language_servers.keys().next().and_then(|ls_name| { syn_loader_conf .language_server - .get(lsp.name()) + .get(ls_name) .map(|config| config.command.clone()) }), )?; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 118836c0..6f7ed174 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -394,13 +394,11 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { let matcher = Matcher::default(); - let language_servers = - doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); - let options = match language_servers - .into_iter() + let options = match doc!(editor) + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) { - Some(id_options) => id_options, + Some(options) => options, None => { return vec![]; } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 60997956..4aa64634 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -202,11 +202,10 @@ fn render_lsp_spinner(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option