diff options
author | Blaž Hrastnik | 2023-05-19 00:39:35 +0000 |
---|---|---|
committer | GitHub | 2023-05-19 00:39:35 +0000 |
commit | 53f47bc47771c94dab51626ca025be28e62eba0c (patch) | |
tree | c8f5c59d40d1ecde227c209f898cc7afd6da5477 /helix-view/src | |
parent | 7f5940be80eaa3aec7903903072b7108f41dd97b (diff) | |
parent | 2a512f7c487f0a707a7eb158e24bd478433bcd91 (diff) |
Merge pull request #2507 from Philipp-M/multiple-language-servers
Add support for multiple language servers per language
Diffstat (limited to 'helix-view/src')
-rw-r--r-- | helix-view/src/document.rs | 112 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 70 | ||||
-rw-r--r-- | helix-view/src/gutter.rs | 36 |
3 files changed, 137 insertions, 81 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index eb376567..bd3c465d 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}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -179,8 +179,8 @@ pub struct Document { version: i32, // should be usize? pub(crate) modified_since_accessed: bool, - diagnostics: Vec<Diagnostic>, - language_server: Option<Arc<helix_lsp::Client>>, + pub(crate) diagnostics: Vec<Diagnostic>, + pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>, diff_handle: Option<DiffHandle>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>, @@ -580,7 +580,7 @@ where *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::lsp; +use helix_lsp::{lsp, Client, LanguageServerName}; use url::Url; impl Document { @@ -616,7 +616,7 @@ impl Document { last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, - language_server: None, + language_servers: HashMap::new(), diff_handle: None, config, version_control_head: None, @@ -730,10 +730,12 @@ impl Document { return Some(formatting_future.boxed()); }; - let language_server = self.language_server()?; let text = self.text.clone(); + // finds first language server that supports formatting and then formats + let language_server = self + .language_servers_with_feature(LanguageServerFeature::Format) + .next()?; let offset_encoding = language_server.offset_encoding(); - let request = language_server.text_document_formatting( self.identifier(), lsp::FormattingOptions { @@ -797,13 +799,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 +848,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?; } @@ -1004,11 +1004,6 @@ impl Document { Ok(()) } - /// Set the LSP. - pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) { - self.language_server = language_server; - } - /// Select text within the [`Document`]. pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { // TODO: use a transaction? @@ -1159,7 +1154,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 +1410,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 +1429,45 @@ 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) + /// maintains the order as configured in the language_servers TOML array + pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> { + self.language_config().into_iter().flat_map(move |config| { + config.language_servers.iter().filter_map(move |features| { + let ls = &**self.language_servers.get(&features.name)?; + if ls.is_initialized() { + Some(ls) + } else { + None + } + }) + }) + } + + pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> { + self.language_servers.remove(name) + } + + pub fn language_servers_with_feature( + &self, + feature: LanguageServerFeature, + ) -> impl Iterator<Item = &helix_lsp::Client> { + self.language_config().into_iter().flat_map(move |config| { + config.language_servers.iter().filter_map(move |features| { + let ls = &**self.language_servers.get(&features.name)?; + if ls.is_initialized() + && ls.supports_feature(feature) + && features.has_feature(feature) + { + Some(ls) + } else { + None + } + }) + }) + } + + pub fn supports_language_server(&self, id: usize) -> bool { + self.language_servers().any(|l| l.id() == id) } pub fn diff_handle(&self) -> Option<&DiffHandle> { @@ -1565,12 +1590,29 @@ impl Document { &self.diagnostics } - pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { - self.diagnostics = diagnostics; + pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator { + self.diagnostics.iter().filter(|d| { + self.language_servers_with_feature(LanguageServerFeature::Diagnostics) + .any(|ls| ls.id() == d.language_server_id) + }) + } + + pub fn replace_diagnostics( + &mut self, + mut diagnostics: Vec<Diagnostic>, + 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..1f27603c 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<KeyEvent>)>, pub macro_replaying: Vec<char>, pub language_servers: helix_lsp::Registry, - pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, + pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>, pub diff_providers: DiffProviderRegistry, pub debugger: Option<dap::Client>, @@ -874,7 +874,7 @@ pub struct Editor { /// times during rendering and should not be set by other functions. pub cursor_cache: Cell<Option<Option<Position>>>, /// When a new completion request is sent to the server old - /// unifinished request must be dropped. Each completion + /// unfinished request must be dropped. Each completion /// request is associated with a channel that cancels /// when the channel is dropped. That channel is stored /// here. When a new completion request is sent this @@ -941,6 +941,7 @@ impl Editor { syn_loader: Arc<syntax::Loader>, config: Arc<dyn DynAccess<Config>>, ) -> 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, @@ -1092,60 +1093,75 @@ impl Editor { self._refresh(); } + #[inline] + pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> { + self.language_servers.get_by_id(language_server_id) + } + /// 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; } - // if doc doesn't have a URL it's a scratch buffer, ignore it - let doc = self.document(doc_id)?; + let doc = self.documents.get_mut(&doc_id)?; + let doc_url = doc.url()?; let (lang, path) = (doc.language.clone(), doc.path().cloned()); 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_servers) = language_servers { + let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); - 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())); - } + // only spawn new language servers if the servers aren't the same - let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); + let doc_language_servers_not_in_registry = + doc.language_servers.iter().filter(|(name, doc_ls)| { + language_servers + .get(*name) + .map_or(true, |ls| ls.id() != doc_ls.id()) + }); + for (_, language_server) in doc_language_servers_not_in_registry { + tokio::spawn(language_server.text_document_did_close(doc.identifier())); + } + + let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| { + doc.language_servers + .get(*name) + .map_or(true, |doc_ls| ls.id() != doc_ls.id()) + }); + + for (_, language_server) in language_servers_not_in_doc { // 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_url.clone(), doc.version(), doc.text(), - language_id, + language_id.clone(), )); - - doc.set_language_server(Some(language_server)); } + + doc.language_servers = language_servers; } Some(()) } @@ -1338,7 +1354,7 @@ 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 _ = self.launch_language_servers(id); id }; @@ -1368,7 +1384,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..a332a8a3 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,5 +1,7 @@ use std::fmt::Write; +use helix_core::syntax::LanguageServerFeature; + use crate::{ editor::GutterType, graphics::{Style, UnderlineStyle}, @@ -55,7 +57,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.diagnostics; Box::new( move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { @@ -63,28 +65,24 @@ pub fn diagnostic<'doc>( return None; } use helix_core::diagnostic::Severity; - if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { - let after = diagnostics[index..].iter().take_while(|d| d.line == line); - - let before = diagnostics[..index] - .iter() - .rev() - .take_while(|d| d.line == line); - - let diagnostics_on_line = after.chain(before); - - // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search. - let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap(); - - write!(out, "●").unwrap(); - return Some(match diagnostic.severity { + let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line); + let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..] + .iter() + .take_while(|d| { + d.line == line + && doc + .language_servers_with_feature(LanguageServerFeature::Diagnostics) + .any(|ls| ls.id() == d.language_server_id) + }); + diagnostics_on_line.max_by_key(|d| d.severity).map(|d| { + write!(out, "●").ok(); + match d.severity { Some(Severity::Error) => error, Some(Severity::Warning) | None => warning, Some(Severity::Info) => info, Some(Severity::Hint) => hint, - }); - } - None + } + }) }, ) } |