summaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
authorPhilipp Mildenberger2022-05-23 16:10:48 +0000
committerPhilipp Mildenberger2023-05-18 19:48:30 +0000
commit71551d395b4e47804df2d8ecea99e34dbbf16157 (patch)
tree042b861690af3288ba81b9bad6fa5675255d6858 /helix-view
parent7f5940be80eaa3aec7903903072b7108f41dd97b (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-view')
-rw-r--r--helix-view/src/document.rs145
-rw-r--r--helix-view/src/editor.rs64
-rw-r--r--helix-view/src/gutter.rs2
3 files changed, 141 insertions, 70 deletions
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<Diagnostic>,
- language_server: Option<Arc<helix_lsp::Client>>,
+ language_servers: Vec<Arc<helix_lsp::Client>>,
diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@@ -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<Arc<helix_lsp::Client>>) {
- self.language_server = language_server;
+ pub fn set_language_servers(&mut self, language_servers: Vec<Arc<helix_lsp::Client>>) {
+ 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<Diagnostic>) {
- self.diagnostics = diagnostics;
+ pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
+ 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<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..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<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, OffsetEncoding)>>,
pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>,
@@ -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,
@@ -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::<Vec<_>>();
Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {