on master help sign in
Merge pull request #2507 from Philipp-M/multiple-language-servers

Add support for multiple language servers per language

commit53f47bc47771c94dab51626ca025be28e62eba0c authorBlaž Hrastnik <blaz@mxxn.io> dateFri 19 May 2023, 09:39 (3 years ago) parent7f5940b2a512f7
22 files changed+1635−1140
book/src/generated/typable-cmd.md+2−2 book/src/guides/adding_languages.md+1 book/src/languages.md+92−17 helix-core/src/diagnostic.rs+1 helix-core/src/syntax.rs+165−13 helix-lsp/src/client.rs+92−13 helix-lsp/src/lib.rs+114−96 helix-lsp/src/transport.rs+43−20 helix-term/src/application.rs+72−73 helix-term/src/commands.rs+117−94 helix-term/src/commands/lsp.rs+432−418 helix-term/src/commands/typed.rs+41−40 helix-term/src/health.rs+15−7 helix-term/src/ui/completion.rs+53−48 helix-term/src/ui/editor.rs+5−12 helix-term/src/ui/mod.rs+7−15 helix-term/src/ui/statusline.rs+5−6 helix-view/src/document.rs+77−35 helix-view/src/editor.rs+43−27 helix-view/src/gutter.rs+17−19 languages.toml+235−180 xtask/src/docgen.rs+6−5
book/src/generated/typable-cmd.md+2−2
@@ -50,8 +50,8 @@
5050| `:reload-all` | Discard changes and reload all documents from the source files. |
5151| `:update`, `:u` | Write changes only if the file has been modified. |
5252| `:lsp-workspace-command` | Open workspace command picker |
53−| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
54−| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
53+| `:lsp-restart` | Restarts the language servers used by the current doc |
54+| `:lsp-stop` | Stops the language servers that are used by the current doc |
5555| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
5656| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
5757| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
book/src/guides/adding_languages.md+1
@@ -9,6 +9,7 @@ below.
99 necessary configuration for the new language. For more information on
1010 language configuration, refer to the
1111 [language configuration section](../languages.md) of the documentation.
12+ A new language server can be added by extending the `[language-server]` table in the same file.
12132. If you are adding a new language or updating an existing language server
1314 configuration, run the command `cargo xtask docgen` to update the
1415 [Language Support](../lang-support.md) documentation.
book/src/languages.md+92−17
@@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
1818```toml
1919# in <config_dir>/helix/languages.toml
2020
21+[language-server.mylang-lsp]
22+command = "mylang-lsp"
23+
2124[[language]]
2225name = "rust"
2326auto-format = false
@@ -41,8 +44,8 @@ injection-regex = "mylang"
4144file-types = ["mylang", "myl"]
4245comment-token = "#"
4346indent = { tab-width = 2, unit = " " }
44−language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
4547formatter = { command = "mylang-formatter" , args = ["--stdin"] }
48+language-servers = [ "mylang-lsp" ]
4649```
4750
4851These configuration keys are available:
@@ -50,6 +53,7 @@ These configuration keys are available:
5053| Key | Description |
5154| ---- | ----------- |
5255| `name` | The name of the language |
56+| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
5357| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
5458| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
5559| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@@ -59,7 +63,7 @@ These configuration keys are available:
5963| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
6064| `comment-token` | The token to use as a comment-token |
6165| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
62−| `language-server` | The Language Server to run. See the Language Server configuration section below. |
66+| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
6367| `config` | Language Server configuration |
6468| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
6569| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
@@ -92,31 +96,102 @@ with the following priorities:
9296 replaced at runtime with the appropriate path separator for the operating
9397 system, so this rule would match against `.git\config` files on Windows.
9498
95−### Language Server configuration
99+## Language Server configuration
100+
101+Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
96102
97−The `language-server` field takes the following keys:
103+For example:
98104
99−| Key | Description |
100−| --- | ----------- |
101−| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
102−| `args` | A list of arguments to pass to the language server binary |
103−| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
104−| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer |
105−| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
105+```toml
106+[language-server.mylang-lsp]
107+command = "mylang-lsp"
108+args = ["--stdio"]
109+config = { provideFormatter = true }
110+environment = { "ENV1" = "value1", "ENV2" = "value2" }
111+
112+[language-server.efm-lsp-prettier]
113+command = "efm-langserver"
114+
115+[language-server.efm-lsp-prettier.config]
116+documentFormatting = true
117+languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
118+```
106119
107−The top-level `config` field is used to configure the LSP initialization options. A `format`
108−sub-table within `config` can be used to pass extra formatting options to
109−[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook).
120+These are the available options for a language server.
121+
122+| Key | Description |
123+| ---- | ----------- |
124+| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
125+| `args` | A list of arguments to pass to the language server binary |
126+| `config` | LSP initialization options |
127+| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
128+| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
129+
130+A `format` sub-table within `config` can be used to pass extra formatting options to
131+[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
110132For example with typescript:
111133
112134```toml
113−[[language]]
114−name = "typescript"
115−auto-format = true
135+[language-server.typescript-language-server]
116136# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
117137config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
118138```
119139
140+### Configuring Language Servers for a language
141+
142+The `language-servers` attribute in a language tells helix which language servers are used for this language.
143+
144+They have to be defined in the `[language-server]` table as described in the previous section.
145+
146+Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
147+
148+In case multiple language servers are specified in the `language-servers` attribute of a `language`,
149+it's often useful to only enable/disable certain language-server features for these language servers.
150+
151+For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
152+so everything else should be handled by the `typescript-language-server` (which is configured by default)
153+The language configuration for typescript could look like this:
154+
155+```toml
156+[[language]]
157+name = "typescript"
158+language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
159+```
160+
161+or equivalent:
162+
163+```toml
164+[[language]]
165+name = "typescript"
166+language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
167+```
168+
169+Each requested LSP feature is prioritized in the order of the `language-servers` array.
170+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`).
171+The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
172+If no `except-features` or `only-features` is given all features for the language server are enabled.
173+If a language server itself doesn't support a feature the next language server array entry will be tried (and so on).
174+
175+The list of supported features is:
176+
177+- `format`
178+- `goto-definition`
179+- `goto-declaration`
180+- `goto-type-definition`
181+- `goto-reference`
182+- `goto-implementation`
183+- `signature-help`
184+- `hover`
185+- `document-highlight`
186+- `completion`
187+- `code-action`
188+- `workspace-command`
189+- `document-symbols`
190+- `workspace-symbols`
191+- `diagnostics`
192+- `rename-symbol`
193+- `inlay-hints`
194+
120195## Tree-sitter grammar configuration
121196
122197The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
helix-core/src/diagnostic.rs+1
@@ -43,6 +43,7 @@ pub struct Diagnostic {
4343 pub message: String,
4444 pub severity: Option<Severity>,
4545 pub code: Option<NumberOrString>,
46+ pub language_server_id: usize,
4647 pub tags: Vec<DiagnosticTag>,
4748 pub source: Option<String>,
4849 pub data: Option<serde_json::Value>,
helix-core/src/syntax.rs+165−13
@@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
1616use std::{
1717 borrow::Cow,
1818 cell::RefCell,
19− collections::{HashMap, VecDeque},
20− fmt,
19+ collections::{HashMap, HashSet, VecDeque},
20+ fmt::{self, Display},
2121 hash::{Hash, Hasher},
2222 mem::{replace, transmute},
2323 path::{Path, PathBuf},
@@ -26,7 +26,7 @@ use std::{
2626};
2727
2828use once_cell::sync::{Lazy, OnceCell};
29−use serde::{Deserialize, Serialize};
29+use serde::{ser::SerializeSeq, Deserialize, Serialize};
3030
3131use helix_loader::grammar::{get_language, load_runtime_file};
3232
@@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
6060}
6161
6262#[derive(Debug, Serialize, Deserialize)]
63+#[serde(rename_all = "kebab-case")]
6364pub struct Configuration {
6465 pub language: Vec<LanguageConfiguration>,
66+ #[serde(default)]
67+ pub language_server: HashMap<String, LanguageServerConfiguration>,
6568}
6669
6770impl Default for Configuration {
@@ -75,7 +78,10 @@ impl Default for Configuration {
7578#[serde(rename_all = "kebab-case", deny_unknown_fields)]
7679pub struct LanguageConfiguration {
7780 #[serde(rename = "name")]
78− pub language_id: String, // c-sharp, rust
81+ pub language_id: String, // c-sharp, rust, tsx
82+ #[serde(rename = "language-id")]
83+ // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
84+ pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
7985 pub scope: String, // source.rust
8086 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
8187 #[serde(default)]
@@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
8591 pub text_width: Option<usize>,
8692 pub soft_wrap: Option<SoftWrap>,
8793
88− #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
89− pub config: Option<serde_json::Value>,
90−
9194 #[serde(default)]
9295 pub auto_format: bool,
9396
@@ -107,8 +110,13 @@ pub struct LanguageConfiguration {
107110 #[serde(skip)]
108111 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
109112 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
110− #[serde(skip_serializing_if = "Option::is_none")]
111− pub language_server: Option<LanguageServerConfiguration>,
113+ #[serde(
114+ default,
115+ skip_serializing_if = "Vec::is_empty",
116+ serialize_with = "serialize_lang_features",
117+ deserialize_with = "deserialize_lang_features"
118+ )]
119+ pub language_servers: Vec<LanguageServerFeatures>,
112120 #[serde(skip_serializing_if = "Option::is_none")]
113121 pub indent: Option<IndentationConfiguration>,
114122
@@ -208,6 +216,133 @@ impl<'de> Deserialize<'de> for FileType {
208216 }
209217}
210218
219+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
220+#[serde(rename_all = "kebab-case")]
221+pub enum LanguageServerFeature {
222+ Format,
223+ GotoDeclaration,
224+ GotoDefinition,
225+ GotoTypeDefinition,
226+ GotoReference,
227+ GotoImplementation,
228+ // Goto, use bitflags, combining previous Goto members?
229+ SignatureHelp,
230+ Hover,
231+ DocumentHighlight,
232+ Completion,
233+ CodeAction,
234+ WorkspaceCommand,
235+ DocumentSymbols,
236+ WorkspaceSymbols,
237+ // Symbols, use bitflags, see above?
238+ Diagnostics,
239+ RenameSymbol,
240+ InlayHints,
241+}
242+
243+impl Display for LanguageServerFeature {
244+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245+ use LanguageServerFeature::*;
246+ let feature = match self {
247+ Format => "format",
248+ GotoDeclaration => "goto-declaration",
249+ GotoDefinition => "goto-definition",
250+ GotoTypeDefinition => "goto-type-definition",
251+ GotoReference => "goto-type-definition",
252+ GotoImplementation => "goto-implementation",
253+ SignatureHelp => "signature-help",
254+ Hover => "hover",
255+ DocumentHighlight => "document-highlight",
256+ Completion => "completion",
257+ CodeAction => "code-action",
258+ WorkspaceCommand => "workspace-command",
259+ DocumentSymbols => "document-symbols",
260+ WorkspaceSymbols => "workspace-symbols",
261+ Diagnostics => "diagnostics",
262+ RenameSymbol => "rename-symbol",
263+ InlayHints => "inlay-hints",
264+ };
265+ write!(f, "{feature}",)
266+ }
267+}
268+
269+#[derive(Debug, Serialize, Deserialize)]
270+#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
271+enum LanguageServerFeatureConfiguration {
272+ #[serde(rename_all = "kebab-case")]
273+ Features {
274+ #[serde(default, skip_serializing_if = "HashSet::is_empty")]
275+ only_features: HashSet<LanguageServerFeature>,
276+ #[serde(default, skip_serializing_if = "HashSet::is_empty")]
277+ except_features: HashSet<LanguageServerFeature>,
278+ name: String,
279+ },
280+ Simple(String),
281+}
282+
283+#[derive(Debug, Default)]
284+pub struct LanguageServerFeatures {
285+ pub name: String,
286+ pub only: HashSet<LanguageServerFeature>,
287+ pub excluded: HashSet<LanguageServerFeature>,
288+}
289+
290+impl LanguageServerFeatures {
291+ pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
292+ (self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
293+ }
294+}
295+
296+fn deserialize_lang_features<'de, D>(
297+ deserializer: D,
298+) -> Result<Vec<LanguageServerFeatures>, D::Error>
299+where
300+ D: serde::Deserializer<'de>,
301+{
302+ let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
303+ let res = raw
304+ .into_iter()
305+ .map(|config| match config {
306+ LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
307+ name,
308+ ..Default::default()
309+ },
310+ LanguageServerFeatureConfiguration::Features {
311+ only_features,
312+ except_features,
313+ name,
314+ } => LanguageServerFeatures {
315+ name,
316+ only: only_features,
317+ excluded: except_features,
318+ },
319+ })
320+ .collect();
321+ Ok(res)
322+}
323+fn serialize_lang_features<S>(
324+ map: &Vec<LanguageServerFeatures>,
325+ serializer: S,
326+) -> Result<S::Ok, S::Error>
327+where
328+ S: serde::Serializer,
329+{
330+ let mut serializer = serializer.serialize_seq(Some(map.len()))?;
331+ for features in map {
332+ let features = if features.only.is_empty() && features.excluded.is_empty() {
333+ LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
334+ } else {
335+ LanguageServerFeatureConfiguration::Features {
336+ only_features: features.only.clone(),
337+ except_features: features.excluded.clone(),
338+ name: features.name.to_owned(),
339+ }
340+ };
341+ serializer.serialize_element(&features)?;
342+ }
343+ serializer.end()
344+}
345+
211346#[derive(Debug, Serialize, Deserialize)]
212347#[serde(rename_all = "kebab-case")]
213348pub struct LanguageServerConfiguration {
@@ -217,9 +352,10 @@ pub struct LanguageServerConfiguration {
217352 pub args: Vec<String>,
218353 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
219354 pub environment: HashMap<String, String>,
355+ #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
356+ pub config: Option<serde_json::Value>,
220357 #[serde(default = "default_timeout")]
221358 pub timeout: u64,
222− pub language_id: Option<String>,
223359}
224360
225361#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -594,6 +730,8 @@ pub struct Loader {
594730 language_config_ids_by_suffix: HashMap<String, usize>,
595731 language_config_ids_by_shebang: HashMap<String, usize>,
596732
733+ language_server_configs: HashMap<String, LanguageServerConfiguration>,
734+
597735 scopes: ArcSwap<Vec<String>>,
598736}
599737
@@ -601,6 +739,7 @@ impl Loader {
601739 pub fn new(config: Configuration) -> Self {
602740 let mut loader = Self {
603741 language_configs: Vec::new(),
742+ language_server_configs: config.language_server,
604743 language_config_ids_by_extension: HashMap::new(),
605744 language_config_ids_by_suffix: HashMap::new(),
606745 language_config_ids_by_shebang: HashMap::new(),
@@ -725,6 +864,10 @@ impl Loader {
725864 self.language_configs.iter()
726865 }
727866
867+ pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
868+ &self.language_server_configs
869+ }
870+
728871 pub fn set_scopes(&self, scopes: Vec<String>) {
729872 self.scopes.store(Arc::new(scopes));
730873
@@ -2370,7 +2513,10 @@ mod test {
23702513 "#,
23712514 );
23722515
2373− let loader = Loader::new(Configuration { language: vec![] });
2516+ let loader = Loader::new(Configuration {
2517+ language: vec![],
2518+ language_server: HashMap::new(),
2519+ });
23742520 let language = get_language("rust").unwrap();
23752521
23762522 let query = Query::new(language, query_str).unwrap();
@@ -2429,7 +2575,10 @@ mod test {
24292575 .map(String::from)
24302576 .collect();
24312577
2432− let loader = Loader::new(Configuration { language: vec![] });
2578+ let loader = Loader::new(Configuration {
2579+ language: vec![],
2580+ language_server: HashMap::new(),
2581+ });
24332582
24342583 let language = get_language("rust").unwrap();
24352584 let config = HighlightConfiguration::new(
@@ -2532,7 +2681,10 @@ mod test {
25322681 ) {
25332682 let source = Rope::from_str(source);
25342683
2535− let loader = Loader::new(Configuration { language: vec![] });
2684+ let loader = Loader::new(Configuration {
2685+ language: vec![],
2686+ language_server: HashMap::new(),
2687+ });
25362688 let language = get_language(language_name).unwrap();
25372689
25382690 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
helix-lsp/src/client.rs+92−13
@@ -4,7 +4,7 @@ use crate::{
44 Call, Error, OffsetEncoding, Result,
55};
66
7−use helix_core::{find_workspace, path, ChangeSet, Rope};
7+use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
88use helix_loader::{self, VERSION_AND_GIT_HASH};
99use lsp::{
1010 notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
4444#[derive(Debug)]
4545pub struct Client {
4646 id: usize,
47+ name: String,
4748 _process: Child,
4849 server_tx: UnboundedSender<Payload>,
4950 request_counter: AtomicU64,
@@ -166,8 +167,7 @@ impl Client {
166167 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167168 }
168169
169− #[allow(clippy::type_complexity)]
170− #[allow(clippy::too_many_arguments)]
170+ #[allow(clippy::type_complexity, clippy::too_many_arguments)]
171171 pub fn start(
172172 cmd: &str,
173173 args: &[String],
@@ -176,6 +176,7 @@ impl Client {
176176 root_markers: &[String],
177177 manual_roots: &[PathBuf],
178178 id: usize,
179+ name: String,
179180 req_timeout: u64,
180181 doc_path: Option<&std::path::PathBuf>,
181182 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@@ -200,7 +201,7 @@ impl Client {
200201 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201202
202203 let (server_rx, server_tx, initialize_notify) =
203− Transport::start(reader, writer, stderr, id);
204+ Transport::start(reader, writer, stderr, id, name.clone());
204205 let (workspace, workspace_is_cwd) = find_workspace();
205206 let workspace = path::get_normalized_path(&workspace);
206207 let root = find_lsp_workspace(
@@ -225,6 +226,7 @@ impl Client {
225226
226227 let client = Self {
227228 id,
229+ name,
228230 _process: process,
229231 server_tx,
230232 request_counter: AtomicU64::new(0),
@@ -240,6 +242,10 @@ impl Client {
240242 Ok((client, server_rx, initialize_notify))
241243 }
242244
245+ pub fn name(&self) -> &str {
246+ &self.name
247+ }
248+
243249 pub fn id(&self) -> usize {
244250 self.id
245251 }
@@ -270,6 +276,87 @@ impl Client {
270276 .expect("language server not yet initialized!")
271277 }
272278
279+ /// Client has to be initialized otherwise this function panics
280+ #[inline]
281+ pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
282+ let capabilities = self.capabilities();
283+
284+ use lsp::*;
285+ match feature {
286+ LanguageServerFeature::Format => matches!(
287+ capabilities.document_formatting_provider,
288+ Some(OneOf::Left(true) | OneOf::Right(_))
289+ ),
290+ LanguageServerFeature::GotoDeclaration => matches!(
291+ capabilities.declaration_provider,
292+ Some(
293+ DeclarationCapability::Simple(true)
294+ | DeclarationCapability::RegistrationOptions(_)
295+ | DeclarationCapability::Options(_),
296+ )
297+ ),
298+ LanguageServerFeature::GotoDefinition => matches!(
299+ capabilities.definition_provider,
300+ Some(OneOf::Left(true) | OneOf::Right(_))
301+ ),
302+ LanguageServerFeature::GotoTypeDefinition => matches!(
303+ capabilities.type_definition_provider,
304+ Some(
305+ TypeDefinitionProviderCapability::Simple(true)
306+ | TypeDefinitionProviderCapability::Options(_),
307+ )
308+ ),
309+ LanguageServerFeature::GotoReference => matches!(
310+ capabilities.references_provider,
311+ Some(OneOf::Left(true) | OneOf::Right(_))
312+ ),
313+ LanguageServerFeature::GotoImplementation => matches!(
314+ capabilities.implementation_provider,
315+ Some(
316+ ImplementationProviderCapability::Simple(true)
317+ | ImplementationProviderCapability::Options(_),
318+ )
319+ ),
320+ LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
321+ LanguageServerFeature::Hover => matches!(
322+ capabilities.hover_provider,
323+ Some(HoverProviderCapability::Simple(true) | HoverProviderCapability::Options(_),)
324+ ),
325+ LanguageServerFeature::DocumentHighlight => matches!(
326+ capabilities.document_highlight_provider,
327+ Some(OneOf::Left(true) | OneOf::Right(_))
328+ ),
329+ LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
330+ LanguageServerFeature::CodeAction => matches!(
331+ capabilities.code_action_provider,
332+ Some(
333+ CodeActionProviderCapability::Simple(true)
334+ | CodeActionProviderCapability::Options(_),
335+ )
336+ ),
337+ LanguageServerFeature::WorkspaceCommand => {
338+ capabilities.execute_command_provider.is_some()
339+ }
340+ LanguageServerFeature::DocumentSymbols => matches!(
341+ capabilities.document_symbol_provider,
342+ Some(OneOf::Left(true) | OneOf::Right(_))
343+ ),
344+ LanguageServerFeature::WorkspaceSymbols => matches!(
345+ capabilities.workspace_symbol_provider,
346+ Some(OneOf::Left(true) | OneOf::Right(_))
347+ ),
348+ LanguageServerFeature::Diagnostics => true, // there's no extra server capability
349+ LanguageServerFeature::RenameSymbol => matches!(
350+ capabilities.rename_provider,
351+ Some(OneOf::Left(true)) | Some(OneOf::Right(_))
352+ ),
353+ LanguageServerFeature::InlayHints => matches!(
354+ capabilities.inlay_hint_provider,
355+ Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
356+ ),
357+ }
358+ }
359+
273360 pub fn offset_encoding(&self) -> OffsetEncoding {
274361 self.capabilities()
275362 .position_encoding
@@ -1295,21 +1382,13 @@ impl Client {
12951382 Some(self.call::<lsp::request::CodeActionRequest>(params))
12961383 }
12971384
1298− pub fn supports_rename(&self) -> bool {
1299− let capabilities = self.capabilities.get().unwrap();
1300− matches!(
1301− capabilities.rename_provider,
1302− Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
1303− )
1304− }
1305−
13061385 pub fn rename_symbol(
13071386 &self,
13081387 text_document: lsp::TextDocumentIdentifier,
13091388 position: lsp::Position,
13101389 new_name: String,
13111390 ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
1312− if !self.supports_rename() {
1391+ if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
13131392 return None;
13141393 }
13151394
helix-lsp/src/lib.rs+114−96
@@ -12,24 +12,21 @@ pub use lsp_types as lsp;
1212use futures_util::stream::select_all::SelectAll;
1313use helix_core::{
1414 path,
15− syntax::{LanguageConfiguration, LanguageServerConfiguration},
15+ syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
1616};
1717use tokio::sync::mpsc::UnboundedReceiver;
1818
1919use std::{
20− collections::{hash_map::Entry, HashMap},
20+ collections::HashMap,
2121 path::{Path, PathBuf},
22− sync::{
23− atomic::{AtomicUsize, Ordering},
24− Arc,
25− },
22+ sync::Arc,
2623};
2724
2825use thiserror::Error;
2926use tokio_stream::wrappers::UnboundedReceiverStream;
3027
3128pub type Result<T> = core::result::Result<T, Error>;
32−type LanguageId = String;
29+pub type LanguageServerName = String;
3330
3431#[derive(Error, Debug)]
3532pub enum Error {
@@ -49,7 +46,7 @@ pub enum Error {
4946 Other(#[from] anyhow::Error),
5047}
5148
52−#[derive(Clone, Copy, Debug, Default)]
49+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
5350pub enum OffsetEncoding {
5451 /// UTF-8 code units aka bytes
5552 Utf8,
@@ -624,23 +621,18 @@ impl Notification {
624621
625622#[derive(Debug)]
626623pub struct Registry {
627− inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
628−
629− counter: AtomicUsize,
624+ inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
625+ syn_loader: Arc<helix_core::syntax::Loader>,
626+ counter: usize,
630627 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
631628}
632629
633−impl Default for Registry {
634− fn default() -> Self {
635− Self::new()
636− }
637−}
638−
639630impl Registry {
640− pub fn new() -> Self {
631+ pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
641632 Self {
642633 inner: HashMap::new(),
643− counter: AtomicUsize::new(0),
634+ syn_loader,
635+ counter: 0,
644636 incoming: SelectAll::new(),
645637 }
646638 }
@@ -649,65 +641,92 @@ impl Registry {
649641 self.inner
650642 .values()
651643 .flatten()
652− .find(|(client_id, _)| client_id == &id)
653− .map(|(_, client)| client.as_ref())
644+ .find(|client| client.id() == id)
645+ .map(|client| &**client)
654646 }
655647
656648 pub fn remove_by_id(&mut self, id: usize) {
657− self.inner.retain(|_, clients| {
658− clients.retain(|&(client_id, _)| client_id != id);
659− !clients.is_empty()
660− })
649+ self.inner.retain(|_, language_servers| {
650+ language_servers.retain(|ls| id != ls.id());
651+ !language_servers.is_empty()
652+ });
661653 }
662654
655+ fn start_client(
656+ &mut self,
657+ name: String,
658+ ls_config: &LanguageConfiguration,
659+ doc_path: Option<&std::path::PathBuf>,
660+ root_dirs: &[PathBuf],
661+ enable_snippets: bool,
662+ ) -> Result<Arc<Client>> {
663+ let config = self
664+ .syn_loader
665+ .language_server_configs()
666+ .get(&name)
667+ .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
668+ let id = self.counter;
669+ self.counter += 1;
670+ let NewClient(client, incoming) = start_client(
671+ id,
672+ name,
673+ ls_config,
674+ config,
675+ doc_path,
676+ root_dirs,
677+ enable_snippets,
678+ )?;
679+ self.incoming.push(UnboundedReceiverStream::new(incoming));
680+ Ok(client)
681+ }
682+
683+ /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
684+ /// as it could be that language servers of these documents were stopped by this method.
685+ /// See helix_view::editor::Editor::refresh_language_servers
663686 pub fn restart(
664687 &mut self,
665688 language_config: &LanguageConfiguration,
666689 doc_path: Option<&std::path::PathBuf>,
667690 root_dirs: &[PathBuf],
668691 enable_snippets: bool,
669− ) -> Result<Option<Arc<Client>>> {
670− let config = match &language_config.language_server {
671− Some(config) => config,
672− None => return Ok(None),
673− };
674−
675− let scope = language_config.scope.clone();
676−
677− match self.inner.entry(scope) {
678− Entry::Vacant(_) => Ok(None),
679− Entry::Occupied(mut entry) => {
680− // initialize a new client
681− let id = self.counter.fetch_add(1, Ordering::Relaxed);
682−
683− let NewClientResult(client, incoming) = start_client(
684− id,
685− language_config,
686− config,
687− doc_path,
688− root_dirs,
689− enable_snippets,
690− )?;
691− self.incoming.push(UnboundedReceiverStream::new(incoming));
692−
693− let old_clients = entry.insert(vec![(id, client.clone())]);
694−
695− for (_, old_client) in old_clients {
696− tokio::spawn(async move {
697− let _ = old_client.force_shutdown().await;
698− });
692+ ) -> Result<Vec<Arc<Client>>> {
693+ language_config
694+ .language_servers
695+ .iter()
696+ .filter_map(|LanguageServerFeatures { name, .. }| {
697+ if self.inner.contains_key(name) {
698+ let client = match self.start_client(
699+ name.clone(),
700+ language_config,
701+ doc_path,
702+ root_dirs,
703+ enable_snippets,
704+ ) {
705+ Ok(client) => client,
706+ error => return Some(error),
707+ };
708+ let old_clients = self
709+ .inner
710+ .insert(name.clone(), vec![client.clone()])
711+ .unwrap();
712+
713+ for old_client in old_clients {
714+ tokio::spawn(async move {
715+ let _ = old_client.force_shutdown().await;
716+ });
717+ }
718+
719+ Some(Ok(client))
720+ } else {
721+ None
699722 }
700−
701− Ok(Some(client))
702− }
703− }
723+ })
724+ .collect()
704725 }
705726
706− pub fn stop(&mut self, language_config: &LanguageConfiguration) {
707− let scope = language_config.scope.clone();
708−
709− if let Some(clients) = self.inner.remove(&scope) {
710− for (_, client) in clients {
727+ pub fn stop(&mut self, name: &str) {
728+ if let Some(clients) = self.inner.remove(name) {
729+ for client in clients {
711730 tokio::spawn(async move {
712731 let _ = client.force_shutdown().await;
713732 });
@@ -721,37 +740,34 @@ impl Registry {
721740 doc_path: Option<&std::path::PathBuf>,
722741 root_dirs: &[PathBuf],
723742 enable_snippets: bool,
724− ) -> Result<Option<Arc<Client>>> {
725− let config = match &language_config.language_server {
726− Some(config) => config,
727− None => return Ok(None),
728− };
729−
730− let clients = self.inner.entry(language_config.scope.clone()).or_default();
731− // check if we already have a client for this documents root that we can reuse
732− if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
733− client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
734− }) {
735− return Ok(Some(client.1.clone()));
736− }
737− // initialize a new client
738− let id = self.counter.fetch_add(1, Ordering::Relaxed);
739−
740− let NewClientResult(client, incoming) = start_client(
741− id,
742− language_config,
743− config,
744− doc_path,
745− root_dirs,
746− enable_snippets,
747− )?;
748− clients.push((id, client.clone()));
749− self.incoming.push(UnboundedReceiverStream::new(incoming));
750− Ok(Some(client))
743+ ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
744+ language_config
745+ .language_servers
746+ .iter()
747+ .map(|LanguageServerFeatures { name, .. }| {
748+ if let Some(clients) = self.inner.get(name) {
749+ if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
750+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
751+ }) {
752+ return Ok((name.to_owned(), client.clone()));
753+ }
754+ }
755+ let client = self.start_client(
756+ name.clone(),
757+ language_config,
758+ doc_path,
759+ root_dirs,
760+ enable_snippets,
761+ )?;
762+ let clients = self.inner.entry(name.clone()).or_default();
763+ clients.push(client.clone());
764+ Ok((name.clone(), client))
765+ })
766+ .collect()
751767 }
752768
753769 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
754− self.inner.values().flatten().map(|(_, client)| client)
770+ self.inner.values().flatten()
755771 }
756772}
757773
@@ -833,26 +849,28 @@ impl LspProgressMap {
833849 }
834850}
835851
836−struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
852+struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
837853
838854/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
839855/// it is only called when it makes sense.
840856fn start_client(
841857 id: usize,
858+ name: String,
842859 config: &LanguageConfiguration,
843860 ls_config: &LanguageServerConfiguration,
844861 doc_path: Option<&std::path::PathBuf>,
845862 root_dirs: &[PathBuf],
846863 enable_snippets: bool,
847−) -> Result<NewClientResult> {
864+) -> Result<NewClient> {
848865 let (client, incoming, initialize_notify) = Client::start(
849866 &ls_config.command,
850867 &ls_config.args,
851− config.config.clone(),
868+ ls_config.config.clone(),
852869 ls_config.environment.clone(),
853870 &config.roots,
854871 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
855872 id,
873+ name,
856874 ls_config.timeout,
857875 doc_path,
858876 )?;
@@ -886,7 +904,7 @@ fn start_client(
886904 initialize_notify.notify_one();
887905 });
888906
889− Ok(NewClientResult(client, incoming))
907+ Ok(NewClient(client, incoming))
890908}
891909
892910/// Find an LSP workspace of a file using the following mechanism:
helix-lsp/src/transport.rs+43−20
@@ -38,6 +38,7 @@ enum ServerMessage {
3838#[derive(Debug)]
3939pub struct Transport {
4040 id: usize,
41+ name: String,
4142 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
4243}
4344
@@ -47,6 +48,7 @@ impl Transport {
4748 server_stdin: BufWriter<ChildStdin>,
4849 server_stderr: BufReader<ChildStderr>,
4950 id: usize,
51+ name: String,
5052 ) -> (
5153 UnboundedReceiver<(usize, jsonrpc::Call)>,
5254 UnboundedSender<Payload>,
@@ -58,6 +60,7 @@ impl Transport {
5860
5961 let transport = Self {
6062 id,
63+ name,
6164 pending_requests: Mutex::new(HashMap::default()),
6265 };
6366
@@ -83,6 +86,7 @@ impl Transport {
8386 async fn recv_server_message(
8487 reader: &mut (impl AsyncBufRead + Unpin + Send),
8588 buffer: &mut String,
89+ language_server_name: &str,
8690 ) -> Result<ServerMessage> {
8791 let mut content_length = None;
8892 loop {
@@ -124,7 +128,7 @@ impl Transport {
124128 reader.read_exact(&mut content).await?;
125129 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
126130
127− info!("<- {}", msg);
131+ info!("{language_server_name} <- {msg}");
128132
129133 // try parsing as output (server response) or call (server request)
130134 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@@ -135,12 +139,13 @@ impl Transport {
135139 async fn recv_server_error(
136140 err: &mut (impl AsyncBufRead + Unpin + Send),
137141 buffer: &mut String,
142+ language_server_name: &str,
138143 ) -> Result<()> {
139144 buffer.truncate(0);
140145 if err.read_line(buffer).await? == 0 {
141146 return Err(Error::StreamClosed);
142147 };
143− error!("err <- {:?}", buffer);
148+ error!("{language_server_name} err <- {buffer:?}");
144149
145150 Ok(())
146151 }
@@ -162,15 +167,17 @@ impl Transport {
162167 Payload::Notification(value) => serde_json::to_string(&value)?,
163168 Payload::Response(error) => serde_json::to_string(&error)?,
164169 };
165− self.send_string_to_server(server_stdin, json).await
170+ self.send_string_to_server(server_stdin, json, &self.name)
171+ .await
166172 }
167173
168174 async fn send_string_to_server(
169175 &self,
170176 server_stdin: &mut BufWriter<ChildStdin>,
171177 request: String,
178+ language_server_name: &str,
172179 ) -> Result<()> {
173− info!("-> {}", request);
180+ info!("{language_server_name} -> {request}");
174181
175182 // send the headers
176183 server_stdin
@@ -189,9 +196,13 @@ impl Transport {
189196 &self,
190197 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
191198 msg: ServerMessage,
199+ language_server_name: &str,
192200 ) -> Result<()> {
193201 match msg {
194− ServerMessage::Output(output) => self.process_request_response(output).await?,
202+ ServerMessage::Output(output) => {
203+ self.process_request_response(output, language_server_name)
204+ .await?
205+ }
195206 ServerMessage::Call(call) => {
196207 client_tx
197208 .send((self.id, call))
@@ -202,14 +213,18 @@ impl Transport {
202213 Ok(())
203214 }
204215
205− async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
216+ async fn process_request_response(
217+ &self,
218+ output: jsonrpc::Output,
219+ language_server_name: &str,
220+ ) -> Result<()> {
206221 let (id, result) = match output {
207222 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
208− info!("<- {}", result);
223+ info!("{language_server_name} <- {}", result);
209224 (id, Ok(result))
210225 }
211226 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
212− error!("<- {}", error);
227+ error!("{language_server_name} <- {error}");
213228 (id, Err(error.into()))
214229 }
215230 };
@@ -240,12 +255,17 @@ impl Transport {
240255 ) {
241256 let mut recv_buffer = String::new();
242257 loop {
243− match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
258+ match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
259+ .await
260+ {
244261 Ok(msg) => {
245− match transport.process_server_message(&client_tx, msg).await {
262+ match transport
263+ .process_server_message(&client_tx, msg, &transport.name)
264+ .await
265+ {
246266 Ok(_) => {}
247267 Err(err) => {
248− error!("err: <- {:?}", err);
268+ error!("{} err: <- {err:?}", transport.name);
249269 break;
250270 }
251271 };
@@ -270,7 +290,7 @@ impl Transport {
270290 params: jsonrpc::Params::None,
271291 }));
272292 match transport
273− .process_server_message(&client_tx, notification)
293+ .process_server_message(&client_tx, notification, &transport.name)
274294 .await
275295 {
276296 Ok(_) => {}
@@ -281,20 +301,22 @@ impl Transport {
281301 break;
282302 }
283303 Err(err) => {
284− error!("err: <- {:?}", err);
304+ error!("{} err: <- {err:?}", transport.name);
285305 break;
286306 }
287307 }
288308 }
289309 }
290310
291− async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
311+ async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
292312 let mut recv_buffer = String::new();
293313 loop {
294− match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
314+ match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
315+ .await
316+ {
295317 Ok(_) => {}
296318 Err(err) => {
297− error!("err: <- {:?}", err);
319+ error!("{} err: <- {err:?}", transport.name);
298320 break;
299321 }
300322 }
@@ -348,10 +370,11 @@ impl Transport {
348370 method: lsp_types::notification::Initialized::METHOD.to_string(),
349371 params: jsonrpc::Params::None,
350372 }));
351− match transport.process_server_message(&client_tx, notification).await {
373+ let language_server_name = &transport.name;
374+ match transport.process_server_message(&client_tx, notification, language_server_name).await {
352375 Ok(_) => {}
353376 Err(err) => {
354− error!("err: <- {:?}", err);
377+ error!("{language_server_name} err: <- {err:?}");
355378 }
356379 }
357380
@@ -361,7 +384,7 @@ impl Transport {
361384 match transport.send_payload_to_server(&mut server_stdin, msg).await {
362385 Ok(_) => {}
363386 Err(err) => {
364− error!("err: <- {:?}", err);
387+ error!("{language_server_name} err: <- {err:?}");
365388 }
366389 }
367390 }
@@ -380,7 +403,7 @@ impl Transport {
380403 match transport.send_payload_to_server(&mut server_stdin, msg).await {
381404 Ok(_) => {}
382405 Err(err) => {
383− error!("err: <- {:?}", err);
406+ error!("{} err: <- {err:?}", transport.name);
384407 }
385408 }
386409 }
helix-term/src/application.rs+72−73
@@ -30,6 +30,7 @@ use crate::{
3030
3131use log::{debug, error, warn};
3232use std::{
33+ collections::btree_map::Entry,
3334 io::{stdin, stdout},
3435 path::Path,
3536 sync::Arc,
@@ -564,7 +565,7 @@ impl Application {
564565 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565566 let id = doc.id();
566567 doc.detect_language(loader);
567− let _ = self.editor.refresh_language_server(id);
568+ self.editor.refresh_language_servers(id);
568569 }
569570
570571 // TODO: fix being overwritten by lsp
@@ -662,6 +663,18 @@ impl Application {
662663 ) {
663664 use helix_lsp::{Call, MethodCall, Notification};
664665
666+ macro_rules! language_server {
667+ () => {
668+ match self.editor.language_server_by_id(server_id) {
669+ Some(language_server) => language_server,
670+ None => {
671+ warn!("can't find language server with id `{}`", server_id);
672+ return;
673+ }
674+ }
675+ };
676+ }
677+
665678 match call {
666679 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
667680 let notification = match Notification::parse(&method, params) {
@@ -677,14 +690,7 @@ impl Application {
677690
678691 match notification {
679692 Notification::Initialized => {
680− let language_server =
681− match self.editor.language_servers.get_by_id(server_id) {
682− Some(language_server) => language_server,
683− None => {
684− warn!("can't find language server with id `{}`", server_id);
685− return;
686− }
687− };
693+ let language_server = language_server!();
688694
689695 // Trigger a workspace/didChangeConfiguration notification after initialization.
690696 // This might not be required by the spec but Neovim does this as well, so it's
@@ -693,9 +699,10 @@ impl Application {
693699 tokio::spawn(language_server.did_change_configuration(config.clone()));
694700 }
695701
696− let docs = self.editor.documents().filter(|doc| {
697− doc.language_server().map(|server| server.id()) == Some(server_id)
698− });
702+ let docs = self
703+ .editor
704+ .documents()
705+ .filter(|doc| doc.supports_language_server(server_id));
699706
700707 // trigger textDocument/didOpen for docs that are already open
701708 for doc in docs {
@@ -715,7 +722,7 @@ impl Application {
715722 ));
716723 }
717724 }
718− Notification::PublishDiagnostics(mut params) => {
725+ Notification::PublishDiagnostics(params) => {
719726 let path = match params.uri.to_file_path() {
720727 Ok(path) => path,
721728 Err(_) => {
@@ -723,6 +730,7 @@ impl Application {
723730 return;
724731 }
725732 };
733+ let offset_encoding = language_server!().offset_encoding();
726734 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
727735 if let Some(version) = params.version {
728736 if version != doc.version() {
@@ -745,18 +753,11 @@ impl Application {
745753 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
746754 use lsp::DiagnosticSeverity;
747755
748− let language_server = if let Some(language_server) = doc.language_server() {
749− language_server
750− } else {
751− log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
752− return None;
753− };
754−
755756 // TODO: convert inside server
756757 let start = if let Some(start) = lsp_pos_to_pos(
757758 text,
758759 diagnostic.range.start,
759− language_server.offset_encoding(),
760+ offset_encoding,
760761 ) {
761762 start
762763 } else {
@@ -764,11 +765,9 @@ impl Application {
764765 return None;
765766 };
766767
767− let end = if let Some(end) = lsp_pos_to_pos(
768− text,
769− diagnostic.range.end,
770− language_server.offset_encoding(),
771− ) {
768+ let end = if let Some(end) =
769+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
770+ {
772771 end
773772 } else {
774773 log::warn!("lsp position out of bounds - {:?}", diagnostic);
@@ -807,14 +806,19 @@ impl Application {
807806 None => None,
808807 };
809808
810− let tags = if let Some(ref tags) = diagnostic.tags {
811− let new_tags = tags.iter().filter_map(|tag| {
812− match *tag {
813− lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
814− lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
815− _ => None
816− }
817− }).collect();
809+ let tags = if let Some(tags) = &diagnostic.tags {
810+ let new_tags = tags
811+ .iter()
812+ .filter_map(|tag| match *tag {
813+ lsp::DiagnosticTag::DEPRECATED => {
814+ Some(DiagnosticTag::Deprecated)
815+ }
816+ lsp::DiagnosticTag::UNNECESSARY => {
817+ Some(DiagnosticTag::Unnecessary)
818+ }
819+ _ => None,
820+ })
821+ .collect();
818822
819823 new_tags
820824 } else {
@@ -830,25 +834,40 @@ impl Application {
830834 tags,
831835 source: diagnostic.source.clone(),
832836 data: diagnostic.data.clone(),
837+ language_server_id: server_id,
833838 })
834839 })
835840 .collect();
836841
837− doc.set_diagnostics(diagnostics);
842+ doc.replace_diagnostics(diagnostics, server_id);
838843 }
839844
840− // Sort diagnostics first by severity and then by line numbers.
841− // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
842− params
845+ let mut diagnostics = params
843846 .diagnostics
844− .sort_unstable_by_key(|d| (d.severity, d.range.start));
847+ .into_iter()
848+ .map(|d| (d, server_id))
849+ .collect();
845850
846851 // Insert the original lsp::Diagnostics here because we may have no open document
847852 // for diagnosic message and so we can't calculate the exact position.
848853 // When using them later in the diagnostics picker, we calculate them on-demand.
849− self.editor
850− .diagnostics
851− .insert(params.uri, params.diagnostics);
854+ match self.editor.diagnostics.entry(params.uri) {
855+ Entry::Occupied(o) => {
856+ let current_diagnostics = o.into_mut();
857+ // there may entries of other language servers, which is why we can't overwrite the whole entry
858+ current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
859+ current_diagnostics.append(&mut diagnostics);
860+ // Sort diagnostics first by severity and then by line numbers.
861+ // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
862+ current_diagnostics
863+ .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
864+ }
865+ Entry::Vacant(v) => {
866+ diagnostics
867+ .sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
868+ v.insert(diagnostics);
869+ }
870+ };
852871 }
853872 Notification::ShowMessage(params) => {
854873 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -950,10 +969,8 @@ impl Application {
950969 .editor
951970 .documents_mut()
952971 .filter_map(|doc| {
953− if doc.language_server().map(|server| server.id())
954− == Some(server_id)
955− {
956− doc.set_diagnostics(Vec::new());
972+ if doc.supports_language_server(server_id) {
973+ doc.clear_diagnostics(server_id);
957974 doc.url()
958975 } else {
959976 None
@@ -1029,31 +1046,21 @@ impl Application {
10291046 }))
10301047 }
10311048 Ok(MethodCall::WorkspaceFolders) => {
1032− let language_server =
1033− self.editor.language_servers.get_by_id(server_id).unwrap();
1034−
1035− Ok(json!(&*language_server.workspace_folders().await))
1049+ Ok(json!(&*language_server!().workspace_folders().await))
10361050 }
10371051 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1052+ let language_server = language_server!();
10381053 let result: Vec<_> = params
10391054 .items
10401055 .iter()
10411056 .map(|item| {
1042− let mut config = match &item.scope_uri {
1043− Some(scope) => {
1044− let path = scope.to_file_path().ok()?;
1045− let doc = self.editor.document_by_path(path)?;
1046− doc.language_config()?.config.as_ref()?
1047− }
1048− None => self
1049− .editor
1050− .language_servers
1051− .get_by_id(server_id)?
1052− .config()?,
1053− };
1057+ let mut config = language_server.config()?;
10541058 if let Some(section) = item.section.as_ref() {
1055− for part in section.split('.') {
1056− config = config.get(part)?;
1059+ // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
1060+ if !section.is_empty() {
1061+ for part in section.split('.') {
1062+ config = config.get(part)?;
1063+ }
10571064 }
10581065 }
10591066 Some(config)
@@ -1074,15 +1081,7 @@ impl Application {
10741081 }
10751082 };
10761083
1077− let language_server = match self.editor.language_servers.get_by_id(server_id) {
1078− Some(language_server) => language_server,
1079− None => {
1080− warn!("can't find language server with id `{}`", server_id);
1081− return;
1082− }
1083− };
1084−
1085− tokio::spawn(language_server.reply(id, reply));
1084+ tokio::spawn(language_server!().reply(id, reply));
10861085 }
10871086 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
10881087 }
helix-term/src/commands.rs+117−94
@@ -23,6 +23,7 @@ use helix_core::{
2323 regex::{self, Regex, RegexBuilder},
2424 search::{self, CharMatcher},
2525 selection, shellwords, surround,
26+ syntax::LanguageServerFeature,
2627 text_annotations::TextAnnotations,
2728 textobject,
2829 tree_sitter::Node,
@@ -54,13 +55,13 @@ use crate::{
5455 job::Callback,
5556 keymap::ReverseKeymap,
5657 ui::{
57− self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
58− Popup, Prompt, PromptEvent,
58+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
59+ FilePicker, Picker, Popup, Prompt, PromptEvent,
5960 },
6061};
6162
6263use crate::job::{self, Jobs};
63−use futures_util::StreamExt;
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
6465use std::{collections::HashMap, fmt, future::Future};
6566use std::{collections::HashSet, num::NonZeroUsize};
6667
@@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
30293030
30303031fn goto_first_diag(cx: &mut Context) {
30313032 let (view, doc) = current!(cx.editor);
3032− let selection = match doc.diagnostics().first() {
3033+ let selection = match doc.shown_diagnostics().next() {
30333034 Some(diag) => Selection::single(diag.range.start, diag.range.end),
30343035 None => return,
30353036 };
@@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
30383039
30393040fn goto_last_diag(cx: &mut Context) {
30403041 let (view, doc) = current!(cx.editor);
3041− let selection = match doc.diagnostics().last() {
3042+ let selection = match doc.shown_diagnostics().last() {
30423043 Some(diag) => Selection::single(diag.range.start, diag.range.end),
30433044 None => return,
30443045 };
@@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
30543055 .cursor(doc.text().slice(..));
30553056
30563057 let diag = doc
3057− .diagnostics()
3058− .iter()
3058+ .shown_diagnostics()
30593059 .find(|diag| diag.range.start > cursor_pos)
3060− .or_else(|| doc.diagnostics().first());
3060+ .or_else(|| doc.shown_diagnostics().next());
30613061
30623062 let selection = match diag {
30633063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
@@ -3075,11 +3075,10 @@ fn goto_prev_diag(cx: &mut Context) {
30753075 .cursor(doc.text().slice(..));
30763076
30773077 let diag = doc
3078− .diagnostics()
3079− .iter()
3078+ .shown_diagnostics()
30803079 .rev()
30813080 .find(|diag| diag.range.start < cursor_pos)
3082− .or_else(|| doc.diagnostics().last());
3081+ .or_else(|| doc.shown_diagnostics().last());
30833082
30843083 let selection = match diag {
30853084 // NOTE: the selection is reversed because we're jumping to the
@@ -3234,23 +3233,19 @@ pub mod insert {
32343233 use helix_lsp::lsp;
32353234 // if ch matches completion char, trigger completion
32363235 let doc = doc_mut!(cx.editor);
3237− let language_server = match doc.language_server() {
3238− Some(language_server) => language_server,
3239− None => return,
3240− };
3241−
3242− let capabilities = language_server.capabilities();
3236+ let trigger_completion = doc
3237+ .language_servers_with_feature(LanguageServerFeature::Completion)
3238+ .any(|ls| {
3239+ // TODO: what if trigger is multiple chars long
3240+ matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
3241+ trigger_characters: Some(triggers),
3242+ ..
3243+ }) if triggers.iter().any(|trigger| trigger.contains(ch)))
3244+ });
32433245
3244− if let Some(lsp::CompletionOptions {
3245− trigger_characters: Some(triggers),
3246− ..
3247− }) = &capabilities.completion_provider
3248− {
3249− // TODO: what if trigger is multiple chars long
3250− if triggers.iter().any(|trigger| trigger.contains(ch)) {
3251− cx.editor.clear_idle_timer();
3252− super::completion(cx);
3253− }
3246+ if trigger_completion {
3247+ cx.editor.clear_idle_timer();
3248+ super::completion(cx);
32543249 }
32553250 }
32563251
@@ -3258,12 +3253,12 @@ pub mod insert {
32583253 use helix_lsp::lsp;
32593254 // if ch matches signature_help char, trigger
32603255 let doc = doc_mut!(cx.editor);
3261− // The language_server!() macro is not used here since it will
3262− // print an "LSP not active for current buffer" message on
3263− // every keypress.
3264− let language_server = match doc.language_server() {
3265− Some(language_server) => language_server,
3266− None => return,
3256+ // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
3257+ let Some(language_server) = doc
3258+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
3259+ .next()
3260+ else {
3261+ return;
32673262 };
32683263
32693264 let capabilities = language_server.capabilities();
@@ -4046,55 +4041,60 @@ fn format_selections(cx: &mut Context) {
40464041 use helix_lsp::{lsp, util::range_to_lsp_range};
40474042
40484043 let (view, doc) = current!(cx.editor);
4044+ let view_id = view.id;
40494045
40504046 // via lsp if available
40514047 // TODO: else via tree-sitter indentation calculations
40524048
4053− let language_server = match doc.language_server() {
4054− Some(language_server) => language_server,
4055− None => return,
4049+ if doc.selection(view_id).len() != 1 {
4050+ cx.editor
4051+ .set_error("format_selections only supports a single selection for now");
4052+ return;
4053+ }
4054+
4055+ // TODO extra LanguageServerFeature::FormatSelections?
4056+ // maybe such that LanguageServerFeature::Format contains it as well
4057+ let Some(language_server) = doc
4058+ .language_servers_with_feature(LanguageServerFeature::Format)
4059+ .find(|ls| {
4060+ matches!(
4061+ ls.capabilities().document_range_formatting_provider,
4062+ Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
4063+ )
4064+ })
4065+ else {
4066+ cx.editor
4067+ .set_error("No configured language server does not support range formatting");
4068+ return;
40564069 };
40574070
4071+ let offset_encoding = language_server.offset_encoding();
40584072 let ranges: Vec<lsp::Range> = doc
4059− .selection(view.id)
4073+ .selection(view_id)
40604074 .iter()
4061− .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
4075+ .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
40624076 .collect();
40634077
4064− if ranges.len() != 1 {
4065− cx.editor
4066− .set_error("format_selections only supports a single selection for now");
4067− return;
4068− }
4069−
40704078 // TODO: handle fails
40714079 // TODO: concurrent map over all ranges
40724080
40734081 let range = ranges[0];
40744082
4075− let request = match language_server.text_document_range_formatting(
4076− doc.identifier(),
4077− range,
4078− lsp::FormattingOptions::default(),
4079− None,
4080− ) {
4081− Some(future) => future,
4082− None => {
4083− cx.editor
4084− .set_error("Language server does not support range formatting");
4085− return;
4086− }
4087− };
4083+ let future = language_server
4084+ .text_document_range_formatting(
4085+ doc.identifier(),
4086+ range,
4087+ lsp::FormattingOptions::default(),
4088+ None,
4089+ )
4090+ .unwrap();
40884091
4089− let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default();
4092+ let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
40904093
4091− let transaction = helix_lsp::util::generate_transaction_from_edits(
4092− doc.text(),
4093− edits,
4094− language_server.offset_encoding(),
4095− );
4094+ let transaction =
4095+ helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
40964096
4097− doc.apply(&transaction, view.id);
4097+ doc.apply(&transaction, view_id);
40984098}
40994099
41004100fn join_selections_impl(cx: &mut Context, select_space: bool) {
@@ -4231,21 +4231,46 @@ pub fn completion(cx: &mut Context) {
42314231 doc.savepoint(view)
42324232 };
42334233
4234− let language_server = match doc.language_server() {
4235− Some(language_server) => language_server,
4236− None => return,
4237− };
4238−
4239− let offset_encoding = language_server.offset_encoding();
42404234 let text = savepoint.text.clone();
42414235 let cursor = savepoint.cursor();
42424236
4243− let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
4237+ let mut seen_language_servers = HashSet::new();
4238+
4239+ let mut futures: FuturesUnordered<_> = doc
4240+ .language_servers_with_feature(LanguageServerFeature::Completion)
4241+ .filter(|ls| seen_language_servers.insert(ls.id()))
4242+ .map(|language_server| {
4243+ let language_server_id = language_server.id();
4244+ let offset_encoding = language_server.offset_encoding();
4245+ let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
4246+ let doc_id = doc.identifier();
4247+ let completion_request = language_server.completion(doc_id, pos, None).unwrap();
4248+
4249+ async move {
4250+ let json = completion_request.await?;
4251+ let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
4252+
4253+ let items = match response {
4254+ Some(lsp::CompletionResponse::Array(items)) => items,
4255+ // TODO: do something with is_incomplete
4256+ Some(lsp::CompletionResponse::List(lsp::CompletionList {
4257+ is_incomplete: _is_incomplete,
4258+ items,
4259+ })) => items,
4260+ None => Vec::new(),
4261+ }
4262+ .into_iter()
4263+ .map(|item| CompletionItem {
4264+ item,
4265+ language_server_id,
4266+ resolved: false,
4267+ })
4268+ .collect();
42444269
4245− let future = match language_server.completion(doc.identifier(), pos, None) {
4246− Some(future) => future,
4247− None => return,
4248− };
4270+ anyhow::Ok(items)
4271+ }
4272+ })
4273+ .collect();
42494274
42504275 // setup a channel that allows the request to be canceled
42514276 let (tx, rx) = oneshot::channel();
@@ -4254,12 +4279,20 @@ pub fn completion(cx: &mut Context) {
42544279 // and the associated request is automatically dropped
42554280 cx.editor.completion_request_handle = Some(tx);
42564281 let future = async move {
4282+ let items_future = async move {
4283+ let mut items = Vec::new();
4284+ // TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
4285+ while let Some(mut lsp_items) = futures.try_next().await? {
4286+ items.append(&mut lsp_items);
4287+ }
4288+ anyhow::Ok(items)
4289+ };
42574290 tokio::select! {
42584291 biased;
42594292 _ = rx => {
4260− Ok(serde_json::Value::Null)
4293+ Ok(Vec::new())
42614294 }
4262− res = future => {
4295+ res = items_future => {
42634296 res
42644297 }
42654298 }
@@ -4293,9 +4326,9 @@ pub fn completion(cx: &mut Context) {
42934326 },
42944327 ));
42954328
4296− cx.callback(
4297− future,
4298− move |editor, compositor, response: Option<lsp::CompletionResponse>| {
4329+ cx.jobs.callback(async move {
4330+ let items = future.await?;
4331+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
42994332 let (view, doc) = current_ref!(editor);
43004333 // check if the completion request is stale.
43014334 //
@@ -4306,16 +4339,6 @@ pub fn completion(cx: &mut Context) {
43064339 return;
43074340 }
43084341
4309− let items = match response {
4310− Some(lsp::CompletionResponse::Array(items)) => items,
4311− // TODO: do something with is_incomplete
4312− Some(lsp::CompletionResponse::List(lsp::CompletionList {
4313− is_incomplete: _is_incomplete,
4314− items,
4315− })) => items,
4316− None => Vec::new(),
4317− };
4318−
43194342 if items.is_empty() {
43204343 // editor.set_error("No completion available");
43214344 return;
@@ -4326,7 +4349,6 @@ pub fn completion(cx: &mut Context) {
43264349 editor,
43274350 savepoint,
43284351 items,
4329− offset_encoding,
43304352 start_offset,
43314353 trigger_offset,
43324354 size,
@@ -4340,8 +4362,9 @@ pub fn completion(cx: &mut Context) {
43404362 {
43414363 compositor.remove(SignatureHelp::ID);
43424364 }
4343− },
4344− );
4365+ };
4366+ Ok(Callback::EditorCompositor(Box::new(call)))
4367+ });
43454368}
43464369
43474370// comments
@@ -5141,7 +5164,7 @@ async fn shell_impl_async(
51415164 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
51425165 .await?;
51435166 }
5144− Ok::<_, anyhow::Error>(())
5167+ anyhow::Ok(())
51455168 });
51465169 let (output, _) = tokio::join! {
51475170 process.wait_with_output(),
helix-term/src/commands/lsp.rs+432−418
@@ -1,4 +1,4 @@
1−use futures_util::FutureExt;
1+use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
22use helix_lsp::{
33 block_on,
44 lsp::{
@@ -6,8 +6,10 @@ use helix_lsp::{
66 NumberOrString,
77 },
88 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
9− OffsetEncoding,
9+ Client, OffsetEncoding,
1010};
11+use serde_json::Value;
12+use tokio_stream::StreamExt;
1113use tui::{
1214 text::{Span, Spans},
1315 widgets::Row,
@@ -15,7 +17,9 @@ use tui::{
1517
1618use super::{align_view, push_jump, Align, Context, Editor, Open};
1719
18−use helix_core::{path, text_annotations::InlineAnnotation, Selection};
20+use helix_core::{
21+ path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
22+};
1923use helix_view::{
2024 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
2125 editor::Action,
@@ -25,6 +29,7 @@ use helix_view::{
2529
2630use crate::{
2731 compositor::{self, Compositor},
32+ job::Callback,
2833 ui::{
2934 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
3035 Popup, PromptEvent,
@@ -32,25 +37,34 @@ use crate::{
3237};
3338
3439use std::{
35− cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
40+ cmp::Ordering,
41+ collections::{BTreeMap, HashSet},
42+ fmt::Write,
43+ future::Future,
44+ path::PathBuf,
45+ sync::Arc,
3646};
3747
38−/// Gets the language server that is attached to a document, and
39−/// if it's not active displays a status message. Using this macro
40−/// in a context where the editor automatically queries the LSP
41−/// (instead of when the user explicitly does so via a keybind like
42−/// `gd`) will spam the "LSP inactive" status message confusingly.
48+/// Gets the first language server that is attached to a document which supports a specific feature.
49+/// If there is no configured language server that supports the feature, this displays a status message.
50+/// Using this macro in a context where the editor automatically queries the LSP
51+/// (instead of when the user explicitly does so via a keybind like `gd`)
52+/// will spam the "No configured language server supports <feature>" status message confusingly.
4353#[macro_export]
44−macro_rules! language_server {
45− ($editor:expr, $doc:expr) => {
46− match $doc.language_server() {
54+macro_rules! language_server_with_feature {
55+ ($editor:expr, $doc:expr, $feature:expr) => {{
56+ let language_server = $doc.language_servers_with_feature($feature).next();
57+ match language_server {
4758 Some(language_server) => language_server,
4859 None => {
49− $editor.set_status("Language server not active for current buffer");
60+ $editor.set_status(format!(
61+ "No configured language server supports {}",
62+ $feature
63+ ));
5064 return;
5165 }
5266 }
53− };
67+ }};
5468}
5569
5670impl ui::menu::Item for lsp::Location {
@@ -87,20 +101,30 @@ impl ui::menu::Item for lsp::Location {
87101 }
88102}
89103
90−impl ui::menu::Item for lsp::SymbolInformation {
104+struct SymbolInformationItem {
105+ symbol: lsp::SymbolInformation,
106+ offset_encoding: OffsetEncoding,
107+}
108+
109+impl ui::menu::Item for SymbolInformationItem {
91110 /// Path to currently focussed document
92111 type Data = Option<lsp::Url>;
93112
94113 fn format(&self, current_doc_path: &Self::Data) -> Row {
95− if current_doc_path.as_ref() == Some(&self.location.uri) {
96− self.name.as_str().into()
114+ if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
115+ self.symbol.name.as_str().into()
97116 } else {
98− match self.location.uri.to_file_path() {
117+ match self.symbol.location.uri.to_file_path() {
99118 Ok(path) => {
100119 let get_relative_path = path::get_relative_path(path.as_path());
101− format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
120+ format!(
121+ "{} ({})",
122+ &self.symbol.name,
123+ get_relative_path.to_string_lossy()
124+ )
125+ .into()
102126 }
103− Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
127+ Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
104128 }
105129 }
106130 }
@@ -116,6 +140,7 @@ struct DiagnosticStyles {
116140struct PickerDiagnostic {
117141 url: lsp::Url,
118142 diag: lsp::Diagnostic,
143+ offset_encoding: OffsetEncoding,
119144}
120145
121146impl ui::menu::Item for PickerDiagnostic {
@@ -211,21 +236,19 @@ fn jump_to_location(
211236 align_view(doc, view, Align::Center);
212237}
213238
214−fn sym_picker(
215− symbols: Vec<lsp::SymbolInformation>,
216− current_path: Option<lsp::Url>,
217− offset_encoding: OffsetEncoding,
218−) -> FilePicker<lsp::SymbolInformation> {
239+type SymbolPicker = FilePicker<SymbolInformationItem>;
240+
241+fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
219242 // TODO: drop current_path comparison and instead use workspace: bool flag?
220243 FilePicker::new(
221244 symbols,
222245 current_path.clone(),
223− move |cx, symbol, action| {
246+ move |cx, item, action| {
224247 let (view, doc) = current!(cx.editor);
225248 push_jump(view, doc);
226249
227− if current_path.as_ref() != Some(&symbol.location.uri) {
228− let uri = &symbol.location.uri;
250+ if current_path.as_ref() != Some(&item.symbol.location.uri) {
251+ let uri = &item.symbol.location.uri;
229252 let path = match uri.to_file_path() {
230253 Ok(path) => path,
231254 Err(_) => {
@@ -245,7 +268,7 @@ fn sym_picker(
245268 let (view, doc) = current!(cx.editor);
246269
247270 if let Some(range) =
248− lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
271+ lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
249272 {
250273 // we flip the range so that the cursor sits on the start of the symbol
251274 // (for example start of the function).
@@ -253,7 +276,7 @@ fn sym_picker(
253276 align_view(doc, view, Align::Center);
254277 }
255278 },
256− move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
279+ move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
257280 )
258281 .truncate_start(false)
259282}
@@ -266,10 +289,9 @@ enum DiagnosticsFormat {
266289
267290fn diag_picker(
268291 cx: &Context,
269− diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
292+ diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
270293 current_path: Option<lsp::Url>,
271294 format: DiagnosticsFormat,
272− offset_encoding: OffsetEncoding,
273295) -> FilePicker<PickerDiagnostic> {
274296 // TODO: drop current_path comparison and instead use workspace: bool flag?
275297
@@ -277,11 +299,15 @@ fn diag_picker(
277299 let mut flat_diag = Vec::new();
278300 for (url, diags) in diagnostics {
279301 flat_diag.reserve(diags.len());
280− for diag in diags {
281− flat_diag.push(PickerDiagnostic {
282− url: url.clone(),
283− diag,
284− });
302+
303+ for (diag, ls) in diags {
304+ if let Some(ls) = cx.editor.language_server_by_id(ls) {
305+ flat_diag.push(PickerDiagnostic {
306+ url: url.clone(),
307+ diag,
308+ offset_encoding: ls.offset_encoding(),
309+ });
310+ }
285311 }
286312 }
287313
@@ -295,7 +321,13 @@ fn diag_picker(
295321 FilePicker::new(
296322 flat_diag,
297323 (styles, format),
298− move |cx, PickerDiagnostic { url, diag }, action| {
324+ move |cx,
325+ PickerDiagnostic {
326+ url,
327+ diag,
328+ offset_encoding,
329+ },
330+ action| {
299331 if current_path.as_ref() == Some(url) {
300332 let (view, doc) = current!(cx.editor);
301333 push_jump(view, doc);
@@ -306,14 +338,14 @@ fn diag_picker(
306338
307339 let (view, doc) = current!(cx.editor);
308340
309− if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
341+ if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
310342 // we flip the range so that the cursor sits on the start of the symbol
311343 // (for example start of the function).
312344 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
313345 align_view(doc, view, Align::Center);
314346 }
315347 },
316− move |_editor, PickerDiagnostic { url, diag }| {
348+ move |_editor, PickerDiagnostic { url, diag, .. }| {
317349 let location = lsp::Location::new(url.clone(), diag.range);
318350 Some(location_to_file_location(&location))
319351 },
@@ -323,126 +355,154 @@ fn diag_picker(
323355
324356pub fn symbol_picker(cx: &mut Context) {
325357 fn nested_to_flat(
326− list: &mut Vec<lsp::SymbolInformation>,
358+ list: &mut Vec<SymbolInformationItem>,
327359 file: &lsp::TextDocumentIdentifier,
328360 symbol: lsp::DocumentSymbol,
361+ offset_encoding: OffsetEncoding,
329362 ) {
330363 #[allow(deprecated)]
331− list.push(lsp::SymbolInformation {
332− name: symbol.name,
333− kind: symbol.kind,
334− tags: symbol.tags,
335− deprecated: symbol.deprecated,
336− location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
337− container_name: None,
364+ list.push(SymbolInformationItem {
365+ symbol: lsp::SymbolInformation {
366+ name: symbol.name,
367+ kind: symbol.kind,
368+ tags: symbol.tags,
369+ deprecated: symbol.deprecated,
370+ location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
371+ container_name: None,
372+ },
373+ offset_encoding,
338374 });
339375 for child in symbol.children.into_iter().flatten() {
340− nested_to_flat(list, file, child);
376+ nested_to_flat(list, file, child, offset_encoding);
341377 }
342378 }
343379 let doc = doc!(cx.editor);
344380
345− let language_server = language_server!(cx.editor, doc);
346− let current_url = doc.url();
347− let offset_encoding = language_server.offset_encoding();
381+ let mut seen_language_servers = HashSet::new();
348382
349− let future = match language_server.document_symbols(doc.identifier()) {
350− Some(future) => future,
351− None => {
352− cx.editor
353− .set_error("Language server does not support document symbols");
354− return;
355− }
356− };
357−
358− cx.callback(
359− future,
360− move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| {
361− if let Some(symbols) = response {
383+ let mut futures: FuturesUnordered<_> = doc
384+ .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
385+ .filter(|ls| seen_language_servers.insert(ls.id()))
386+ .map(|language_server| {
387+ let request = language_server.document_symbols(doc.identifier()).unwrap();
388+ let offset_encoding = language_server.offset_encoding();
389+ let doc_id = doc.identifier();
390+
391+ async move {
392+ let json = request.await?;
393+ let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
394+ let symbols = match response {
395+ Some(symbols) => symbols,
396+ None => return anyhow::Ok(vec![]),
397+ };
362398 // lsp has two ways to represent symbols (flat/nested)
363399 // convert the nested variant to flat, so that we have a homogeneous list
364400 let symbols = match symbols {
365− lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
401+ lsp::DocumentSymbolResponse::Flat(symbols) => symbols
402+ .into_iter()
403+ .map(|symbol| SymbolInformationItem {
404+ symbol,
405+ offset_encoding,
406+ })
407+ .collect(),
366408 lsp::DocumentSymbolResponse::Nested(symbols) => {
367− let doc = doc!(editor);
368409 let mut flat_symbols = Vec::new();
369410 for symbol in symbols {
370− nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
411+ nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
371412 }
372413 flat_symbols
373414 }
374415 };
375−
376− let picker = sym_picker(symbols, current_url, offset_encoding);
377− compositor.push(Box::new(overlaid(picker)))
416+ Ok(symbols)
378417 }
379− },
380− )
418+ })
419+ .collect();
420+ let current_url = doc.url();
421+
422+ if futures.is_empty() {
423+ cx.editor
424+ .set_error("No configured language server supports document symbols");
425+ return;
426+ }
427+
428+ cx.jobs.callback(async move {
429+ let mut symbols = Vec::new();
430+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
431+ while let Some(mut lsp_items) = futures.try_next().await? {
432+ symbols.append(&mut lsp_items);
433+ }
434+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
435+ let picker = sym_picker(symbols, current_url);
436+ compositor.push(Box::new(overlaid(picker)))
437+ };
438+
439+ Ok(Callback::EditorCompositor(Box::new(call)))
440+ });
381441}
382442
383443pub fn workspace_symbol_picker(cx: &mut Context) {
384444 let doc = doc!(cx.editor);
385− let current_url = doc.url();
386− let language_server = language_server!(cx.editor, doc);
387− let offset_encoding = language_server.offset_encoding();
388− let future = match language_server.workspace_symbols("".to_string()) {
389− Some(future) => future,
390− None => {
391− cx.editor
392− .set_error("Language server does not support workspace symbols");
393− return;
445+
446+ let get_symbols = move |pattern: String, editor: &mut Editor| {
447+ let doc = doc!(editor);
448+ let mut seen_language_servers = HashSet::new();
449+ let mut futures: FuturesUnordered<_> = doc
450+ .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
451+ .filter(|ls| seen_language_servers.insert(ls.id()))
452+ .map(|language_server| {
453+ let request = language_server.workspace_symbols(pattern.clone()).unwrap();
454+ let offset_encoding = language_server.offset_encoding();
455+ async move {
456+ let json = request.await?;
457+
458+ let response =
459+ serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
460+ .unwrap_or_default()
461+ .into_iter()
462+ .map(|symbol| SymbolInformationItem {
463+ symbol,
464+ offset_encoding,
465+ })
466+ .collect();
467+
468+ anyhow::Ok(response)
469+ }
470+ })
471+ .collect();
472+
473+ if futures.is_empty() {
474+ editor.set_error("No configured language server supports workspace symbols");
394475 }
395− };
396476
397− cx.callback(
398− future,
399− move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
400− let symbols = response.unwrap_or_default();
401− let picker = sym_picker(symbols, current_url, offset_encoding);
402− let get_symbols = |query: String, editor: &mut Editor| {
403− let doc = doc!(editor);
404− let language_server = match doc.language_server() {
405− Some(s) => s,
406− None => {
407− // This should not generally happen since the picker will not
408− // even open in the first place if there is no server.
409− return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
410− }
411− };
412− let symbol_request = match language_server.workspace_symbols(query) {
413− Some(future) => future,
414− None => {
415− // This should also not happen since the language server must have
416− // supported workspace symbols before to reach this block.
417− return async move {
418− Err(anyhow::anyhow!(
419− "Language server does not support workspace symbols"
420− ))
421− }
422− .boxed();
423− }
424− };
477+ async move {
478+ let mut symbols = Vec::new();
479+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
480+ while let Some(mut lsp_items) = futures.try_next().await? {
481+ symbols.append(&mut lsp_items);
482+ }
483+ anyhow::Ok(symbols)
484+ }
485+ .boxed()
486+ };
425487
426− let future = async move {
427− let json = symbol_request.await?;
428− let response: Option<Vec<lsp::SymbolInformation>> =
429− serde_json::from_value(json)?;
488+ let current_url = doc.url();
489+ let initial_symbols = get_symbols("".to_owned(), cx.editor);
430490
431− Ok(response.unwrap_or_default())
432− };
433− future.boxed()
434− };
491+ cx.jobs.callback(async move {
492+ let symbols = initial_symbols.await?;
493+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
494+ let picker = sym_picker(symbols, current_url);
435495 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
436496 compositor.push(Box::new(overlaid(dyn_picker)))
437− },
438− )
497+ };
498+
499+ Ok(Callback::EditorCompositor(Box::new(call)))
500+ });
439501}
440502
441503pub fn diagnostics_picker(cx: &mut Context) {
442504 let doc = doc!(cx.editor);
443− let language_server = language_server!(cx.editor, doc);
444505 if let Some(current_url) = doc.url() {
445− let offset_encoding = language_server.offset_encoding();
446506 let diagnostics = cx
447507 .editor
448508 .diagnostics
@@ -454,7 +514,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
454514 [(current_url.clone(), diagnostics)].into(),
455515 Some(current_url),
456516 DiagnosticsFormat::HideSourcePath,
457− offset_encoding,
458517 );
459518 cx.push_layer(Box::new(overlaid(picker)));
460519 }
@@ -462,24 +521,27 @@ pub fn diagnostics_picker(cx: &mut Context) {
462521
463522pub fn workspace_diagnostics_picker(cx: &mut Context) {
464523 let doc = doc!(cx.editor);
465− let language_server = language_server!(cx.editor, doc);
466524 let current_url = doc.url();
467− let offset_encoding = language_server.offset_encoding();
525+ // TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
468526 let diagnostics = cx.editor.diagnostics.clone();
469527 let picker = diag_picker(
470528 cx,
471529 diagnostics,
472530 current_url,
473531 DiagnosticsFormat::ShowSourcePath,
474− offset_encoding,
475532 );
476533 cx.push_layer(Box::new(overlaid(picker)));
477534}
478535
479−impl ui::menu::Item for lsp::CodeActionOrCommand {
536+struct CodeActionOrCommandItem {
537+ lsp_item: lsp::CodeActionOrCommand,
538+ language_server_id: usize,
539+}
540+
541+impl ui::menu::Item for CodeActionOrCommandItem {
480542 type Data = ();
481543 fn format(&self, _data: &Self::Data) -> Row {
482− match self {
544+ match &self.lsp_item {
483545 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
484546 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
485547 }
@@ -546,45 +608,42 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
546608pub fn code_action(cx: &mut Context) {
547609 let (view, doc) = current!(cx.editor);
548610
549− let language_server = language_server!(cx.editor, doc);
550−
551611 let selection_range = doc.selection(view.id).primary();
552− let offset_encoding = language_server.offset_encoding();
553612
554− let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
613+ let mut seen_language_servers = HashSet::new();
555614
556− let future = match language_server.code_actions(
557− doc.identifier(),
558− range,
559− // Filter and convert overlapping diagnostics
560− lsp::CodeActionContext {
561− diagnostics: doc
562− .diagnostics()
563− .iter()
564− .filter(|&diag| {
565− selection_range
566− .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
567− })
568− .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
569− .collect(),
570− only: None,
571− trigger_kind: Some(CodeActionTriggerKind::INVOKED),
572− },
573− ) {
574− Some(future) => future,
575− None => {
576− cx.editor
577− .set_error("Language server does not support code actions");
578− return;
579− }
580− };
581−
582− cx.callback(
583− future,
584− move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
615+ let mut futures: FuturesUnordered<_> = doc
616+ .language_servers_with_feature(LanguageServerFeature::CodeAction)
617+ .filter(|ls| seen_language_servers.insert(ls.id()))
618+ // TODO this should probably already been filtered in something like "language_servers_with_feature"
619+ .filter_map(|language_server| {
620+ let offset_encoding = language_server.offset_encoding();
621+ let language_server_id = language_server.id();
622+ let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
623+ // Filter and convert overlapping diagnostics
624+ let code_action_context = lsp::CodeActionContext {
625+ diagnostics: doc
626+ .diagnostics()
627+ .iter()
628+ .filter(|&diag| {
629+ selection_range
630+ .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
631+ })
632+ .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
633+ .collect(),
634+ only: None,
635+ trigger_kind: Some(CodeActionTriggerKind::INVOKED),
636+ };
637+ let code_action_request =
638+ language_server.code_actions(doc.identifier(), range, code_action_context)?;
639+ Some((code_action_request, language_server_id))
640+ })
641+ .map(|(request, ls_id)| async move {
642+ let json = request.await?;
643+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
585644 let mut actions = match response {
586645 Some(a) => a,
587− None => return,
646+ None => return anyhow::Ok(Vec::new()),
588647 };
589648
590649 // remove disabled code actions
@@ -596,11 +655,6 @@ pub fn code_action(cx: &mut Context) {
596655 )
597656 });
598657
599− if actions.is_empty() {
600− editor.set_status("No code actions available");
601− return;
602− }
603−
604658 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
605659 // Many details are modeled after vscode because language servers are usually tested against it.
606660 // VScode sorts the codeaction two times:
@@ -636,18 +690,51 @@ pub fn code_action(cx: &mut Context) {
636690 .reverse()
637691 });
638692
639− let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
693+ Ok(actions
694+ .into_iter()
695+ .map(|lsp_item| CodeActionOrCommandItem {
696+ lsp_item,
697+ language_server_id: ls_id,
698+ })
699+ .collect())
700+ })
701+ .collect();
702+
703+ if futures.is_empty() {
704+ cx.editor
705+ .set_error("No configured language server supports code actions");
706+ return;
707+ }
708+
709+ cx.jobs.callback(async move {
710+ let mut actions = Vec::new();
711+ // TODO if one code action request errors, all other requests are ignored (even if they're valid)
712+ while let Some(mut lsp_items) = futures.try_next().await? {
713+ actions.append(&mut lsp_items);
714+ }
715+
716+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
717+ if actions.is_empty() {
718+ editor.set_error("No code actions available");
719+ return;
720+ }
721+ let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
640722 if event != PromptEvent::Validate {
641723 return;
642724 }
643725
644726 // always present here
645− let code_action = code_action.unwrap();
727+ let action = action.unwrap();
728+ let Some(language_server) = editor.language_server_by_id(action.language_server_id) else {
729+ editor.set_error("Language Server disappeared");
730+ return;
731+ };
732+ let offset_encoding = language_server.offset_encoding();
646733
647− match code_action {
734+ match &action.lsp_item {
648735 lsp::CodeActionOrCommand::Command(command) => {
649736 log::debug!("code action command: {:?}", command);
650− execute_lsp_command(editor, command.clone());
737+ execute_lsp_command(editor, action.language_server_id, command.clone());
651738 }
652739 lsp::CodeActionOrCommand::CodeAction(code_action) => {
653740 log::debug!("code action: {:?}", code_action);
@@ -659,7 +746,7 @@ pub fn code_action(cx: &mut Context) {
659746 // if code action provides both edit and command first the edit
660747 // should be applied and then the command
661748 if let Some(command) = &code_action.command {
662− execute_lsp_command(editor, command.clone());
749+ execute_lsp_command(editor, action.language_server_id, command.clone());
663750 }
664751 }
665752 }
@@ -668,8 +755,10 @@ pub fn code_action(cx: &mut Context) {
668755
669756 let popup = Popup::new("code-action", picker).with_scrollbar(false);
670757 compositor.replace_or_push("code-action", popup);
671− },
672− )
758+ };
759+
760+ Ok(Callback::EditorCompositor(Box::new(call)))
761+ });
673762}
674763
675764impl ui::menu::Item for lsp::Command {
@@ -679,13 +768,13 @@ impl ui::menu::Item for lsp::Command {
679768 }
680769}
681770
682−pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
683− let doc = doc!(editor);
684− let language_server = language_server!(editor, doc);
685−
771+pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
686772 // the command is executed on the server and communicated back
687773 // to the client asynchronously using workspace edits
688− let future = match language_server.command(cmd) {
774+ let future = match editor
775+ .language_server_by_id(language_server_id)
776+ .and_then(|language_server| language_server.command(cmd))
777+ {
689778 Some(future) => future,
690779 None => {
691780 editor.set_error("Language server does not support executing commands");
@@ -977,21 +1066,17 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
9771066 }
9781067}
9791068
980−pub fn goto_declaration(cx: &mut Context) {
1069+fn goto_single_impl<P, F>(cx: &mut Context, feature: LanguageServerFeature, request_provider: P)
1070+where
1071+ P: Fn(&Client, lsp::Position, lsp::TextDocumentIdentifier) -> Option<F>,
1072+ F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
1073+{
9811074 let (view, doc) = current!(cx.editor);
982− let language_server = language_server!(cx.editor, doc);
983− let offset_encoding = language_server.offset_encoding();
9841075
1076+ let language_server = language_server_with_feature!(cx.editor, doc, feature);
1077+ let offset_encoding = language_server.offset_encoding();
9851078 let pos = doc.position(view.id, offset_encoding);
986−
987− let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
988− Some(future) => future,
989− None => {
990− cx.editor
991− .set_error("Language server does not support goto-declaration");
992− return;
993− }
994− };
1079+ let future = request_provider(language_server, pos, doc.identifier()).unwrap();
9951080
9961081 cx.callback(
9971082 future,
@@ -1002,102 +1087,56 @@ pub fn goto_declaration(cx: &mut Context) {
10021087 );
10031088}
10041089
1005−pub fn goto_definition(cx: &mut Context) {
1006− let (view, doc) = current!(cx.editor);
1007− let language_server = language_server!(cx.editor, doc);
1008− let offset_encoding = language_server.offset_encoding();
1009−
1010− let pos = doc.position(view.id, offset_encoding);
1011−
1012− let future = match language_server.goto_definition(doc.identifier(), pos, None) {
1013− Some(future) => future,
1014− None => {
1015− cx.editor
1016− .set_error("Language server does not support goto-definition");
1017− return;
1018− }
1019− };
1090+pub fn goto_declaration(cx: &mut Context) {
1091+ goto_single_impl(
1092+ cx,
1093+ LanguageServerFeature::GotoDeclaration,
1094+ |ls, pos, doc_id| ls.goto_declaration(doc_id, pos, None),
1095+ );
1096+}
10201097
1021− cx.callback(
1022− future,
1023− move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
1024− let items = to_locations(response);
1025− goto_impl(editor, compositor, items, offset_encoding);
1026− },
1098+pub fn goto_definition(cx: &mut Context) {
1099+ goto_single_impl(
1100+ cx,
1101+ LanguageServerFeature::GotoDefinition,
1102+ |ls, pos, doc_id| ls.goto_definition(doc_id, pos, None),
10271103 );
10281104}
10291105
10301106pub fn goto_type_definition(cx: &mut Context) {
1031− let (view, doc) = current!(cx.editor);
1032− let language_server = language_server!(cx.editor, doc);
1033− let offset_encoding = language_server.offset_encoding();
1034−
1035− let pos = doc.position(view.id, offset_encoding);
1036−
1037− let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
1038− Some(future) => future,
1039− None => {
1040− cx.editor
1041− .set_error("Language server does not support goto-type-definition");
1042− return;
1043− }
1044− };
1045−
1046− cx.callback(
1047− future,
1048− move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
1049− let items = to_locations(response);
1050− goto_impl(editor, compositor, items, offset_encoding);
1051− },
1107+ goto_single_impl(
1108+ cx,
1109+ LanguageServerFeature::GotoTypeDefinition,
1110+ |ls, pos, doc_id| ls.goto_type_definition(doc_id, pos, None),
10521111 );
10531112}
10541113
10551114pub fn goto_implementation(cx: &mut Context) {
1056− let (view, doc) = current!(cx.editor);
1057− let language_server = language_server!(cx.editor, doc);
1058− let offset_encoding = language_server.offset_encoding();
1059−
1060− let pos = doc.position(view.id, offset_encoding);
1061−
1062− let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
1063− Some(future) => future,
1064− None => {
1065− cx.editor
1066− .set_error("Language server does not support goto-implementation");
1067− return;
1068− }
1069− };
1070−
1071− cx.callback(
1072− future,
1073− move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
1074− let items = to_locations(response);
1075− goto_impl(editor, compositor, items, offset_encoding);
1076− },
1115+ goto_single_impl(
1116+ cx,
1117+ LanguageServerFeature::GotoImplementation,
1118+ |ls, pos, doc_id| ls.goto_implementation(doc_id, pos, None),
10771119 );
10781120}
10791121
10801122pub fn goto_reference(cx: &mut Context) {
10811123 let config = cx.editor.config();
10821124 let (view, doc) = current!(cx.editor);
1083− let language_server = language_server!(cx.editor, doc);
1084− let offset_encoding = language_server.offset_encoding();
10851125
1126+ // TODO could probably support multiple language servers,
1127+ // not sure if there's a real practical use case for this though
1128+ let language_server =
1129+ language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference);
1130+ let offset_encoding = language_server.offset_encoding();
10861131 let pos = doc.position(view.id, offset_encoding);
1087−
1088− let future = match language_server.goto_reference(
1089− doc.identifier(),
1090− pos,
1091− config.lsp.goto_reference_include_declaration,
1092− None,
1093− ) {
1094− Some(future) => future,
1095− None => {
1096− cx.editor
1097− .set_error("Language server does not support goto-reference");
1098− return;
1099− }
1100− };
1132+ let future = language_server
1133+ .goto_reference(
1134+ doc.identifier(),
1135+ pos,
1136+ config.lsp.goto_reference_include_declaration,
1137+ None,
1138+ )
1139+ .unwrap();
11011140
11021141 cx.callback(
11031142 future,
@@ -1108,7 +1147,7 @@ pub fn goto_reference(cx: &mut Context) {
11081147 );
11091148}
11101149
1111−#[derive(PartialEq, Eq)]
1150+#[derive(PartialEq, Eq, Clone, Copy)]
11121151pub enum SignatureHelpInvoked {
11131152 Manual,
11141153 Automatic,
@@ -1120,35 +1159,31 @@ pub fn signature_help(cx: &mut Context) {
11201159
11211160pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11221161 let (view, doc) = current!(cx.editor);
1123− let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
1124−
1125− let language_server = match doc.language_server() {
1126− Some(language_server) => language_server,
1127− None => {
1128− // Do not show the message if signature help was invoked
1129− // automatically on backspace, trigger characters, etc.
1130− if was_manually_invoked {
1131− cx.editor
1132− .set_status("Language server not active for current buffer");
1133− }
1134− return;
1135− }
1136− };
1137− let offset_encoding = language_server.offset_encoding();
11381162
1139− let pos = doc.position(view.id, offset_encoding);
1163+ // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
1164+ let future = doc
1165+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
1166+ .find_map(|language_server| {
1167+ let pos = doc.position(view.id, language_server.offset_encoding());
1168+ language_server.text_document_signature_help(doc.identifier(), pos, None)
1169+ });
11401170
1141− let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
1142− Some(f) => f,
1143− None => {
1144− if was_manually_invoked {
1145− cx.editor
1146− .set_error("Language server does not support signature-help");
1147− }
1148− return;
1171+ let Some(future) = future else {
1172+ // Do not show the message if signature help was invoked
1173+ // automatically on backspace, trigger characters, etc.
1174+ if invoked == SignatureHelpInvoked::Manual {
1175+ cx.editor.set_error("No configured language server supports signature-help");
11491176 }
1177+ return;
11501178 };
1179+ signature_help_impl_with_future(cx, future.boxed(), invoked);
1180+}
11511181
1182+pub fn signature_help_impl_with_future(
1183+ cx: &mut Context,
1184+ future: BoxFuture<'static, helix_lsp::Result<Value>>,
1185+ invoked: SignatureHelpInvoked,
1186+) {
11521187 cx.callback(
11531188 future,
11541189 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@@ -1156,7 +1191,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11561191
11571192 if !(config.lsp.auto_signature_help
11581193 || SignatureHelp::visible_popup(compositor).is_some()
1159− || was_manually_invoked)
1194+ || invoked == SignatureHelpInvoked::Manual)
11601195 {
11611196 return;
11621197 }
@@ -1165,7 +1200,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11651200 // it very probably means the server was a little slow to respond and the user has
11661201 // already moved on to something else, making a signature help popup will just be an
11671202 // annoyance, see https://github.com/helix-editor/helix/issues/3112
1168− if !was_manually_invoked && editor.mode != Mode::Insert {
1203+ if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
11691204 return;
11701205 }
11711206
@@ -1255,21 +1290,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
12551290
12561291pub fn hover(cx: &mut Context) {
12571292 let (view, doc) = current!(cx.editor);
1258− let language_server = language_server!(cx.editor, doc);
1259− let offset_encoding = language_server.offset_encoding();
12601293
1294+ // TODO support multiple language servers (merge UI somehow)
1295+ let language_server =
1296+ language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover);
12611297 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
1262−
1263− let pos = doc.position(view.id, offset_encoding);
1264−
1265− let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
1266− Some(future) => future,
1267− None => {
1268− cx.editor
1269− .set_error("Language server does not support hover");
1270− return;
1271− }
1272− };
1298+ let pos = doc.position(view.id, language_server.offset_encoding());
1299+ let future = language_server
1300+ .text_document_hover(doc.identifier(), pos, None)
1301+ .unwrap();
12731302
12741303 cx.callback(
12751304 future,
@@ -1349,7 +1378,11 @@ pub fn rename_symbol(cx: &mut Context) {
13491378 }
13501379 }
13511380
1352− fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
1381+ fn create_rename_prompt(
1382+ editor: &Editor,
1383+ prefill: String,
1384+ language_server_id: Option<usize>,
1385+ ) -> Box<ui::Prompt> {
13531386 let prompt = ui::Prompt::new(
13541387 "rename-to:".into(),
13551388 None,
@@ -1358,22 +1391,22 @@ pub fn rename_symbol(cx: &mut Context) {
13581391 if event != PromptEvent::Validate {
13591392 return;
13601393 }
1361−
13621394 let (view, doc) = current!(cx.editor);
1363− let language_server = language_server!(cx.editor, doc);
1364− let offset_encoding = language_server.offset_encoding();
13651395
1396+ let Some(language_server) = doc
1397+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1398+ .find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
1399+ else {
1400+ cx.editor.set_error("No configured language server supports symbol renaming");
1401+ return;
1402+ };
1403+
1404+ let offset_encoding = language_server.offset_encoding();
13661405 let pos = doc.position(view.id, offset_encoding);
1406+ let future = language_server
1407+ .rename_symbol(doc.identifier(), pos, input.to_string())
1408+ .unwrap();
13671409
1368− let future =
1369− match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
1370− Some(future) => future,
1371− None => {
1372− cx.editor
1373− .set_error("Language server does not support symbol renaming");
1374− return;
1375− }
1376− };
13771410 match block_on(future) {
13781411 Ok(edits) => {
13791412 let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
@@ -1387,21 +1420,28 @@ pub fn rename_symbol(cx: &mut Context) {
13871420 Box::new(prompt)
13881421 }
13891422
1390− let (view, doc) = current!(cx.editor);
1391− let language_server = language_server!(cx.editor, doc);
1392− let offset_encoding = language_server.offset_encoding();
1393−
1394− if !language_server.supports_rename() {
1395− cx.editor
1396− .set_error("Language server does not support symbol renaming");
1397− return;
1398− }
1399−
1400− let pos = doc.position(view.id, offset_encoding);
1423+ let (view, doc) = current_ref!(cx.editor);
1424+
1425+ let language_server_with_prepare_rename_support = doc
1426+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1427+ .find(|ls| {
1428+ matches!(
1429+ ls.capabilities().rename_provider,
1430+ Some(lsp::OneOf::Right(lsp::RenameOptions {
1431+ prepare_provider: Some(true),
1432+ ..
1433+ }))
1434+ )
1435+ });
14011436
1402− match language_server.prepare_rename(doc.identifier(), pos) {
1403− // Language server supports textDocument/prepareRename, use it.
1404− Some(future) => cx.callback(
1437+ if let Some(language_server) = language_server_with_prepare_rename_support {
1438+ let ls_id = language_server.id();
1439+ let offset_encoding = language_server.offset_encoding();
1440+ let pos = doc.position(view.id, offset_encoding);
1441+ let future = language_server
1442+ .prepare_rename(doc.identifier(), pos)
1443+ .unwrap();
1444+ cx.callback(
14051445 future,
14061446 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
14071447 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1413,39 +1453,27 @@ pub fn rename_symbol(cx: &mut Context) {
14131453 }
14141454 };
14151455
1416− let prompt = create_rename_prompt(editor, prefill);
1456+ let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
14171457
14181458 compositor.push(prompt);
14191459 },
1420− ),
1421− // Language server does not support textDocument/prepareRename, fall back
1422− // to word boundary selection.
1423− None => {
1424− let prefill = get_prefill_from_word_boundary(cx.editor);
1425−
1426− let prompt = create_rename_prompt(cx.editor, prefill);
1427−
1428− cx.push_layer(prompt);
1429− }
1430− };
1460+ );
1461+ } else {
1462+ let prefill = get_prefill_from_word_boundary(cx.editor);
1463+ let prompt = create_rename_prompt(cx.editor, prefill, None);
1464+ cx.push_layer(prompt);
1465+ }
14311466}
14321467
14331468pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
14341469 let (view, doc) = current!(cx.editor);
1435− let language_server = language_server!(cx.editor, doc);
1470+ let language_server =
1471+ language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight);
14361472 let offset_encoding = language_server.offset_encoding();
1437−
14381473 let pos = doc.position(view.id, offset_encoding);
1439−
1440− let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
1441− {
1442− Some(future) => future,
1443− None => {
1444− cx.editor
1445− .set_error("Language server does not support document highlight");
1446− return;
1447− }
1448− };
1474+ let future = language_server
1475+ .text_document_document_highlight(doc.identifier(), pos, None)
1476+ .unwrap();
14491477
14501478 cx.callback(
14511479 future,
@@ -1455,8 +1483,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
14551483 _ => return,
14561484 };
14571485 let (view, doc) = current!(editor);
1458− let language_server = language_server!(editor, doc);
1459− let offset_encoding = language_server.offset_encoding();
14601486 let text = doc.text();
14611487 let pos = doc.selection(view.id).primary().head;
14621488
@@ -1502,63 +1528,51 @@ fn compute_inlay_hints_for_view(
15021528 let view_id = view.id;
15031529 let doc_id = view.doc;
15041530
1505− let language_server = doc.language_server()?;
1506−
1507− let capabilities = language_server.capabilities();
1508−
1509− let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
1510− Some(
1511− lsp::OneOf::Left(true)
1512− | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
1513− ) => {
1514− let doc_text = doc.text();
1515− let len_lines = doc_text.len_lines();
1516−
1517− // Compute ~3 times the current view height of inlay hints, that way some scrolling
1518− // will not show half the view with hints and half without while still being faster
1519− // than computing all the hints for the full file (which could be dozens of time
1520− // longer than the view is).
1521− let view_height = view.inner_height();
1522− let first_visible_line =
1523− doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1524− let first_line = first_visible_line.saturating_sub(view_height);
1525− let last_line = first_visible_line
1526− .saturating_add(view_height.saturating_mul(2))
1527− .min(len_lines);
1528−
1529− let new_doc_inlay_hint_id = DocumentInlayHintsId {
1530− first_line,
1531− last_line,
1532− };
1533− // Don't recompute the annotations in case nothing has changed about the view
1534− if !doc.inlay_hints_oudated
1535− && doc
1536− .inlay_hints(view_id)
1537− .map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
1538− {
1539− return None;
1540− }
1531+ let language_server = doc
1532+ .language_servers_with_feature(LanguageServerFeature::InlayHints)
1533+ .next()?;
1534+
1535+ let doc_text = doc.text();
1536+ let len_lines = doc_text.len_lines();
1537+
1538+ // Compute ~3 times the current view height of inlay hints, that way some scrolling
1539+ // will not show half the view with hints and half without while still being faster
1540+ // than computing all the hints for the full file (which could be dozens of time
1541+ // longer than the view is).
1542+ let view_height = view.inner_height();
1543+ let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1544+ let first_line = first_visible_line.saturating_sub(view_height);
1545+ let last_line = first_visible_line
1546+ .saturating_add(view_height.saturating_mul(2))
1547+ .min(len_lines);
1548+
1549+ let new_doc_inlay_hints_id = DocumentInlayHintsId {
1550+ first_line,
1551+ last_line,
1552+ };
1553+ // Don't recompute the annotations in case nothing has changed about the view
1554+ if !doc.inlay_hints_oudated
1555+ && doc
1556+ .inlay_hints(view_id)
1557+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
1558+ {
1559+ return None;
1560+ }
15411561
1542− let doc_slice = doc_text.slice(..);
1543− let first_char_in_range = doc_slice.line_to_char(first_line);
1544− let last_char_in_range = doc_slice.line_to_char(last_line);
1562+ let doc_slice = doc_text.slice(..);
1563+ let first_char_in_range = doc_slice.line_to_char(first_line);
1564+ let last_char_in_range = doc_slice.line_to_char(last_line);
15451565
1546− let range = helix_lsp::util::range_to_lsp_range(
1547− doc_text,
1548− helix_core::Range::new(first_char_in_range, last_char_in_range),
1549− language_server.offset_encoding(),
1550− );
1566+ let range = helix_lsp::util::range_to_lsp_range(
1567+ doc_text,
1568+ helix_core::Range::new(first_char_in_range, last_char_in_range),
1569+ language_server.offset_encoding(),
1570+ );
15511571
1552− (
1553− language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
1554− new_doc_inlay_hint_id,
1555− )
1556− }
1557− _ => return None,
1558− };
1572+ let offset_encoding = language_server.offset_encoding();
15591573
15601574 let callback = super::make_job_callback(
1561− future?,
1575+ language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
15621576 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
15631577 // The config was modified or the window was closed while the request was in flight
15641578 if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
@@ -1572,8 +1586,8 @@ fn compute_inlay_hints_for_view(
15721586 };
15731587
15741588 // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
1575− let (mut hints, offset_encoding) = match (response, doc.language_server()) {
1576− (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
1589+ let mut hints = match response {
1590+ Some(hints) if !hints.is_empty() => hints,
15771591 _ => {
15781592 doc.set_inlay_hints(
15791593 view_id,
helix-term/src/commands/typed.rs+41−40
@@ -1329,26 +1329,22 @@ fn lsp_workspace_command(
13291329 if event != PromptEvent::Validate {
13301330 return Ok(());
13311331 }
1332−
1333− let (_, doc) = current!(cx.editor);
1334−
1335− let language_server = match doc.language_server() {
1336− Some(language_server) => language_server,
1337− None => {
1338− cx.editor
1339− .set_status("Language server not active for current buffer");
1340− return Ok(());
1341− }
1332+ let doc = doc!(cx.editor);
1333+ let Some((language_server_id, options)) = doc
1334+ .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
1335+ .find_map(|ls| {
1336+ ls.capabilities()
1337+ .execute_command_provider
1338+ .as_ref()
1339+ .map(|options| (ls.id(), options))
1340+ })
1341+ else {
1342+ cx.editor.set_status(
1343+ "No active language servers for this document support workspace commands",
1344+ );
1345+ return Ok(());
13421346 };
13431347
1344− let options = match &language_server.capabilities().execute_command_provider {
1345− Some(options) => options,
1346− None => {
1347− cx.editor
1348− .set_status("Workspace commands are not supported for this language server");
1349− return Ok(());
1350− }
1351− };
13521348 if args.is_empty() {
13531349 let commands = options
13541350 .commands
@@ -1362,8 +1358,8 @@ fn lsp_workspace_command(
13621358 let callback = async move {
13631359 let call: job::Callback = Callback::EditorCompositor(Box::new(
13641360 move |_editor: &mut Editor, compositor: &mut Compositor| {
1365− let picker = ui::Picker::new(commands, (), |cx, command, _action| {
1366− execute_lsp_command(cx.editor, command.clone());
1361+ let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
1362+ execute_lsp_command(cx.editor, language_server_id, command.clone());
13671363 });
13681364 compositor.push(Box::new(overlaid(picker)))
13691365 },
@@ -1376,6 +1372,7 @@ fn lsp_workspace_command(
13761372 if options.commands.iter().any(|c| c == &command) {
13771373 execute_lsp_command(
13781374 cx.editor,
1375+ language_server_id,
13791376 helix_lsp::lsp::Command {
13801377 title: command.clone(),
13811378 arguments: None,
@@ -1407,7 +1404,6 @@ fn lsp_restart(
14071404 .language_config()
14081405 .context("LSP not defined for the current document")?;
14091406
1410− let scope = config.scope.clone();
14111407 cx.editor.language_servers.restart(
14121408 config,
14131409 doc.path(),
@@ -1420,13 +1416,22 @@ fn lsp_restart(
14201416 .editor
14211417 .documents()
14221418 .filter_map(|doc| match doc.language_config() {
1423− Some(config) if config.scope.eq(&scope) => Some(doc.id()),
1419+ Some(config)
1420+ if config.language_servers.iter().any(|ls| {
1421+ config
1422+ .language_servers
1423+ .iter()
1424+ .any(|restarted_ls| restarted_ls.name == ls.name)
1425+ }) =>
1426+ {
1427+ Some(doc.id())
1428+ }
14241429 _ => None,
14251430 })
14261431 .collect();
14271432
14281433 for document_id in document_ids_to_refresh {
1429− cx.editor.refresh_language_server(document_id);
1434+ cx.editor.refresh_language_servers(document_id);
14301435 }
14311436
14321437 Ok(())
@@ -1441,22 +1446,18 @@ fn lsp_stop(
14411446 return Ok(());
14421447 }
14431448
1444− let doc = doc!(cx.editor);
1449+ let ls_shutdown_names = doc!(cx.editor)
1450+ .language_servers()
1451+ .map(|ls| ls.name().to_string())
1452+ .collect::<Vec<_>>();
14451453
1446− let ls_id = doc
1447− .language_server()
1448− .map(|ls| ls.id())
1449− .context("LSP not running for the current document")?;
1454+ for ls_name in &ls_shutdown_names {
1455+ cx.editor.language_servers.stop(ls_name);
14501456
1451− let config = doc
1452− .language_config()
1453− .context("LSP not defined for the current document")?;
1454− cx.editor.language_servers.stop(config);
1455−
1456− for doc in cx.editor.documents_mut() {
1457− if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
1458− doc.set_language_server(None);
1459− doc.set_diagnostics(Default::default());
1457+ for doc in cx.editor.documents_mut() {
1458+ if let Some(client) = doc.remove_language_server_by_name(ls_name) {
1459+ doc.clear_diagnostics(client.id());
1460+ }
14601461 }
14611462 }
14621463
@@ -1850,7 +1851,7 @@ fn language(
18501851 doc.detect_indent_and_line_ending();
18511852
18521853 let id = doc.id();
1853− cx.editor.refresh_language_server(id);
1854+ cx.editor.refresh_language_servers(id);
18541855 Ok(())
18551856}
18561857
@@ -2588,14 +2589,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
25882589 TypableCommand {
25892590 name: "lsp-restart",
25902591 aliases: &[],
2591− doc: "Restarts the Language Server that is in use by the current doc",
2592+ doc: "Restarts the language servers used by the current doc",
25922593 fun: lsp_restart,
25932594 signature: CommandSignature::none(),
25942595 },
25952596 TypableCommand {
25962597 name: "lsp-stop",
25972598 aliases: &[],
2598− doc: "Stops the Language Server that is in use by the current doc",
2599+ doc: "Stops the language servers that are used by the current doc",
25992600 fun: lsp_stop,
26002601 signature: CommandSignature::none(),
26012602 },
helix-term/src/health.rs+15−7
@@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> {
192192 for lang in &syn_loader_conf.language {
193193 column(&lang.language_id, Color::Reset);
194194
195− let lsp = lang
196− .language_server
197− .as_ref()
198− .map(|lsp| lsp.command.to_string());
195+ // TODO multiple language servers (check binary for each supported language server, not just the first)
196+
197+ let lsp = lang.language_servers.first().and_then(|ls| {
198+ syn_loader_conf
199+ .language_server
200+ .get(&ls.name)
201+ .map(|config| config.command.clone())
202+ });
199203 check_binary(lsp);
200204
201205 let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@@ -264,11 +268,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
264268 }
265269 };
266270
271+ // TODO multiple language servers
267272 probe_protocol(
268273 "language server",
269− lang.language_server
270− .as_ref()
271− .map(|lsp| lsp.command.to_string()),
274+ lang.language_servers.first().and_then(|ls| {
275+ syn_loader_conf
276+ .language_server
277+ .get(&ls.name)
278+ .map(|config| config.command.clone())
279+ }),
272280 )?;
273281
274282 probe_protocol(
helix-term/src/ui/completion.rs+53−48
@@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
1515use crate::commands;
1616use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
1717
18−use helix_lsp::{lsp, util};
18+use helix_lsp::{lsp, util, OffsetEncoding};
1919
2020impl menu::Item for CompletionItem {
2121 type Data = ();
@@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
3838 || self.item.tags.as_ref().map_or(false, |tags| {
3939 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
4040 });
41+
4142 menu::Row::new(vec![
4243 menu::Cell::from(Span::styled(
4344 self.item.label.as_str(),
@@ -79,19 +80,15 @@ impl menu::Item for CompletionItem {
7980 }
8081 None => "",
8182 }),
82− // self.detail.as_deref().unwrap_or("")
83− // self.label_details
84− // .as_ref()
85− // .or(self.detail())
86− // .as_str(),
8783 ])
8884 }
8985}
9086
9187#[derive(Debug, PartialEq, Default, Clone)]
92−struct CompletionItem {
93− item: lsp::CompletionItem,
94− resolved: bool,
88+pub struct CompletionItem {
89+ pub item: lsp::CompletionItem,
90+ pub language_server_id: usize,
91+ pub resolved: bool,
9592}
9693
9794/// Wraps a Menu.
@@ -109,29 +106,21 @@ impl Completion {
109106 pub fn new(
110107 editor: &Editor,
111108 savepoint: Arc<SavePoint>,
112− mut items: Vec<lsp::CompletionItem>,
113− offset_encoding: helix_lsp::OffsetEncoding,
109+ mut items: Vec<CompletionItem>,
114110 start_offset: usize,
115111 trigger_offset: usize,
116112 ) -> Self {
117113 let replace_mode = editor.config().completion_replace;
118114 // Sort completion items according to their preselect status (given by the LSP server)
119− items.sort_by_key(|item| !item.preselect.unwrap_or(false));
120− let items = items
121− .into_iter()
122− .map(|item| CompletionItem {
123− item,
124− resolved: false,
125− })
126− .collect();
115+ items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
127116
128117 // Then create the menu
129118 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
130119 fn item_to_transaction(
131120 doc: &Document,
132121 view_id: ViewId,
133− item: &CompletionItem,
134− offset_encoding: helix_lsp::OffsetEncoding,
122+ item: &lsp::CompletionItem,
123+ offset_encoding: OffsetEncoding,
135124 trigger_offset: usize,
136125 include_placeholder: bool,
137126 replace_mode: bool,
@@ -141,7 +130,7 @@ impl Completion {
141130 let text = doc.text().slice(..);
142131 let primary_cursor = selection.primary().cursor(text);
143132
144− let (edit_offset, new_text) = if let Some(edit) = &item.item.text_edit {
133+ let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
145134 let edit = match edit {
146135 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
147136 lsp::CompletionTextEdit::InsertAndReplace(item) => {
@@ -164,10 +153,9 @@ impl Completion {
164153 (Some((start_offset, end_offset)), edit.new_text)
165154 } else {
166155 let new_text = item
167− .item
168156 .insert_text
169157 .clone()
170− .unwrap_or_else(|| item.item.label.clone());
158+ .unwrap_or_else(|| item.label.clone());
171159 // check that we are still at the correct savepoint
172160 // we can still generate a transaction regardless but if the
173161 // document changed (and not just the selection) then we will
@@ -176,9 +164,9 @@ impl Completion {
176164 (None, new_text)
177165 };
178166
179− if matches!(item.item.kind, Some(lsp::CompletionItemKind::SNIPPET))
167+ if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
180168 || matches!(
181− item.item.insert_text_format,
169+ item.insert_text_format,
182170 Some(lsp::InsertTextFormat::SNIPPET)
183171 )
184172 {
@@ -223,6 +211,23 @@ impl Completion {
223211
224212 let (view, doc) = current!(editor);
225213
214+ macro_rules! language_server {
215+ ($item:expr) => {
216+ match editor
217+ .language_servers
218+ .get_by_id($item.language_server_id)
219+ {
220+ Some(ls) => ls,
221+ None => {
222+ editor.set_error("completions are outdated");
223+ // TODO close the completion menu somehow,
224+ // currently there is no trivial way to access the EditorView to close the completion menu
225+ return;
226+ }
227+ }
228+ };
229+ }
230+
226231 match event {
227232 PromptEvent::Abort => {}
228233 PromptEvent::Update => {
@@ -250,8 +255,8 @@ impl Completion {
250255 let transaction = item_to_transaction(
251256 doc,
252257 view.id,
253− item,
254− offset_encoding,
258+ &item.item,
259+ language_server!(item).offset_encoding(),
255260 trigger_offset,
256261 true,
257262 replace_mode,
@@ -267,10 +272,18 @@ impl Completion {
267272 // always present here
268273 let mut item = item.unwrap().clone();
269274
275+ let language_server = language_server!(item);
276+ let offset_encoding = language_server.offset_encoding();
277+
278+ let language_server = editor
279+ .language_servers
280+ .get_by_id(item.language_server_id)
281+ .unwrap();
282+
270283 // resolve item if not yet resolved
271284 if !item.resolved {
272285 if let Some(resolved) =
273− Self::resolve_completion_item(doc, item.item.clone())
286+ Self::resolve_completion_item(language_server, item.item.clone())
274287 {
275288 item.item = resolved;
276289 }
@@ -280,7 +293,7 @@ impl Completion {
280293 let transaction = item_to_transaction(
281294 doc,
282295 view.id,
283− &item,
296+ &item.item,
284297 offset_encoding,
285298 trigger_offset,
286299 false,
@@ -323,11 +336,9 @@ impl Completion {
323336 }
324337
325338 fn resolve_completion_item(
326− doc: &Document,
339+ language_server: &helix_lsp::Client,
327340 completion_item: lsp::CompletionItem,
328341 ) -> Option<lsp::CompletionItem> {
329− let language_server = doc.language_server()?;
330−
331342 let future = language_server.resolve_completion_item(completion_item)?;
332343 let response = helix_lsp::block_on(future);
333344 match response {
@@ -398,16 +409,10 @@ impl Completion {
398409 _ => return false,
399410 };
400411
401− let language_server = match doc!(cx.editor).language_server() {
402− Some(language_server) => language_server,
403− None => return false,
404− };
412+ let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
405413
406414 // This method should not block the compositor so we handle the response asynchronously.
407− let future = match language_server.resolve_completion_item(current_item.item.clone()) {
408− Some(future) => future,
409− None => return false,
410− };
415+ let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
411416
412417 cx.callback(
413418 future,
@@ -422,13 +427,13 @@ impl Completion {
422427 .unwrap()
423428 .completion
424429 {
425− completion.replace_item(
426− current_item,
427− CompletionItem {
428− item: resolved_item,
429− resolved: true,
430− },
431− );
430+ let resolved_item = CompletionItem {
431+ item: resolved_item,
432+ language_server_id: current_item.language_server_id,
433+ resolved: true,
434+ };
435+
436+ completion.replace_item(current_item, resolved_item);
432437 }
433438 },
434439 );
helix-term/src/ui/editor.rs+5−12
@@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
3333
3434use tui::{buffer::Buffer as Surface, text::Span};
3535
36−use super::statusline;
36+use super::{completion::CompletionItem, statusline};
3737use super::{document::LineDecoration, lsp::SignatureHelp};
3838
3939pub struct EditorView {
@@ -650,7 +650,7 @@ impl EditorView {
650650 .primary()
651651 .cursor(doc.text().slice(..));
652652
653− let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
653+ let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
654654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
655655 });
656656
@@ -953,20 +953,13 @@ impl EditorView {
953953 &mut self,
954954 editor: &mut Editor,
955955 savepoint: Arc<SavePoint>,
956− items: Vec<helix_lsp::lsp::CompletionItem>,
957− offset_encoding: helix_lsp::OffsetEncoding,
956+ items: Vec<CompletionItem>,
958957 start_offset: usize,
959958 trigger_offset: usize,
960959 size: Rect,
961960 ) -> Option<Rect> {
962− let mut completion = Completion::new(
963− editor,
964− savepoint,
965− items,
966− offset_encoding,
967− start_offset,
968− trigger_offset,
969− );
961+ let mut completion =
962+ Completion::new(editor, savepoint, items, start_offset, trigger_offset);
970963
971964 if completion.is_empty() {
972965 // skip if we got no completion results
helix-term/src/ui/mod.rs+7−15
@@ -17,7 +17,7 @@ mod text;
1717use crate::compositor::{Component, Compositor};
1818use crate::filter_picker_entry;
1919use crate::job::{self, Callback};
20−pub use completion::Completion;
20+pub use completion::{Completion, CompletionItem};
2121pub use editor::EditorView;
2222pub use markdown::Markdown;
2323pub use menu::Menu;
@@ -238,6 +238,7 @@ pub mod completers {
238238 use crate::ui::prompt::Completion;
239239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
240240 use fuzzy_matcher::FuzzyMatcher;
241+ use helix_core::syntax::LanguageServerFeature;
241242 use helix_view::document::SCRATCH_BUFFER_NAME;
242243 use helix_view::theme;
243244 use helix_view::{editor::Config, Editor};
@@ -393,20 +394,11 @@ pub mod completers {
393394 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394395 let matcher = Matcher::default();
395396
396− let (_, doc) = current_ref!(editor);
397−
398− let language_server = match doc.language_server() {
399− Some(language_server) => language_server,
400− None => {
401− return vec![];
402− }
403− };
404−
405− let options = match &language_server.capabilities().execute_command_provider {
406− Some(options) => options,
407− None => {
408− return vec![];
409− }
397+ let Some(options) = doc!(editor)
398+ .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
399+ .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
400+ else {
401+ return vec![];
410402 };
411403
412404 let mut matches: Vec<_> = options
helix-term/src/ui/statusline.rs+5−6
@@ -197,15 +197,15 @@ where
197197 );
198198}
199199
200+// TODO think about handling multiple language servers
200201fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201202where
202203 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203204{
205+ let language_server = context.doc.language_servers().next();
204206 write(
205207 context,
206− context
207− .doc
208− .language_server()
208+ language_server
209209 .and_then(|srv| {
210210 context
211211 .spinners
@@ -225,8 +225,7 @@ where
225225{
226226 let (warnings, errors) = context
227227 .doc
228− .diagnostics()
229− .iter()
228+ .shown_diagnostics()
230229 .fold((0, 0), |mut counts, diag| {
231230 use helix_core::diagnostic::Severity;
232231 match diag.severity {
@@ -266,7 +265,7 @@ where
266265 .diagnostics
267266 .values()
268267 .flatten()
269− .fold((0, 0), |mut counts, diag| {
268+ .fold((0, 0), |mut counts, (diag, _)| {
270269 match diag.severity {
271270 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
272271 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
helix-view/src/document.rs+77−35
@@ -6,7 +6,7 @@ use futures_util::FutureExt;
66use helix_core::auto_pairs::AutoPairs;
77use helix_core::doc_formatter::TextFormat;
88use helix_core::encoding::Encoding;
9−use helix_core::syntax::Highlight;
9+use helix_core::syntax::{Highlight, LanguageServerFeature};
1010use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
1111use helix_core::Range;
1212use helix_vcs::{DiffHandle, DiffProviderRegistry};
@@ -179,8 +179,8 @@ pub struct Document {
179179 version: i32, // should be usize?
180180 pub(crate) modified_since_accessed: bool,
181181
182− diagnostics: Vec<Diagnostic>,
183− language_server: Option<Arc<helix_lsp::Client>>,
182+ pub(crate) diagnostics: Vec<Diagnostic>,
183+ pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
184184
185185 diff_handle: Option<DiffHandle>,
186186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@@ -580,7 +580,7 @@ where
580580 *mut_ref = f(mem::take(mut_ref));
581581}
582582
583−use helix_lsp::lsp;
583+use helix_lsp::{lsp, Client, LanguageServerName};
584584use url::Url;
585585
586586impl Document {
@@ -616,7 +616,7 @@ impl Document {
616616 last_saved_time: SystemTime::now(),
617617 last_saved_revision: 0,
618618 modified_since_accessed: false,
619− language_server: None,
619+ language_servers: HashMap::new(),
620620 diff_handle: None,
621621 config,
622622 version_control_head: None,
@@ -730,10 +730,12 @@ impl Document {
730730 return Some(formatting_future.boxed());
731731 };
732732
733− let language_server = self.language_server()?;
734733 let text = self.text.clone();
734+ // finds first language server that supports formatting and then formats
735+ let language_server = self
736+ .language_servers_with_feature(LanguageServerFeature::Format)
737+ .next()?;
735738 let offset_encoding = language_server.offset_encoding();
736−
737739 let request = language_server.text_document_formatting(
738740 self.identifier(),
739741 lsp::FormattingOptions {
@@ -797,13 +799,12 @@ impl Document {
797799 if self.path.is_none() {
798800 bail!("Can't save with no path set!");
799801 }
800−
801802 self.path.as_ref().unwrap().clone()
802803 }
803804 };
804805
805806 let identifier = self.path().map(|_| self.identifier());
806− let language_server = self.language_server.clone();
807+ let language_servers = self.language_servers.clone();
807808
808809 // mark changes up to now as saved
809810 let current_rev = self.get_current_revision();
@@ -847,14 +848,13 @@ impl Document {
847848 text: text.clone(),
848849 };
849850
850− if let Some(language_server) = language_server {
851+ for (_, language_server) in language_servers {
851852 if !language_server.is_initialized() {
852853 return Ok(event);
853854 }
854−
855− if let Some(identifier) = identifier {
855+ if let Some(identifier) = &identifier {
856856 if let Some(notification) =
857− language_server.text_document_did_save(identifier, &text)
857+ language_server.text_document_did_save(identifier.clone(), &text)
858858 {
859859 notification.await?;
860860 }
@@ -1004,11 +1004,6 @@ impl Document {
10041004 Ok(())
10051005 }
10061006
1007− /// Set the LSP.
1008− pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
1009− self.language_server = language_server;
1010− }
1011−
10121007 /// Select text within the [`Document`].
10131008 pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
10141009 // TODO: use a transaction?
@@ -1159,7 +1154,7 @@ impl Document {
11591154
11601155 if emit_lsp_notification {
11611156 // emit lsp notification
1162− if let Some(language_server) = self.language_server() {
1157+ for language_server in self.language_servers() {
11631158 let notify = language_server.text_document_did_change(
11641159 self.versioned_identifier(),
11651160 &old_doc,
@@ -1415,18 +1410,13 @@ impl Document {
14151410 .map(|language| language.language_id.as_str())
14161411 }
14171412
1418− /// Language ID for the document. Either the `language-id` from the
1419− /// `language-server` configuration, or the document language if no
1420− /// `language-id` has been specified.
1413+ /// Language ID for the document. Either the `language-id`,
1414+ /// or the document language name if no `language-id` has been specified.
14211415 pub fn language_id(&self) -> Option<&str> {
1422− let language_config = self.language.as_deref()?;
1423−
1424− language_config
1425− .language_server
1426− .as_ref()?
1427− .language_id
1416+ self.language_config()?
1417+ .language_server_language_id
14281418 .as_deref()
1429− .or(Some(language_config.language_id.as_str()))
1419+ .or_else(|| self.language_name())
14301420 }
14311421
14321422 /// Corresponding [`LanguageConfiguration`].
@@ -1439,10 +1429,45 @@ impl Document {
14391429 self.version
14401430 }
14411431
1442− /// Language server if it has been initialized.
1443− pub fn language_server(&self) -> Option<&helix_lsp::Client> {
1444− let server = self.language_server.as_deref()?;
1445− server.is_initialized().then_some(server)
1432+ /// maintains the order as configured in the language_servers TOML array
1433+ pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
1434+ self.language_config().into_iter().flat_map(move |config| {
1435+ config.language_servers.iter().filter_map(move |features| {
1436+ let ls = &**self.language_servers.get(&features.name)?;
1437+ if ls.is_initialized() {
1438+ Some(ls)
1439+ } else {
1440+ None
1441+ }
1442+ })
1443+ })
1444+ }
1445+
1446+ pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> {
1447+ self.language_servers.remove(name)
1448+ }
1449+
1450+ pub fn language_servers_with_feature(
1451+ &self,
1452+ feature: LanguageServerFeature,
1453+ ) -> impl Iterator<Item = &helix_lsp::Client> {
1454+ self.language_config().into_iter().flat_map(move |config| {
1455+ config.language_servers.iter().filter_map(move |features| {
1456+ let ls = &**self.language_servers.get(&features.name)?;
1457+ if ls.is_initialized()
1458+ && ls.supports_feature(feature)
1459+ && features.has_feature(feature)
1460+ {
1461+ Some(ls)
1462+ } else {
1463+ None
1464+ }
1465+ })
1466+ })
1467+ }
1468+
1469+ pub fn supports_language_server(&self, id: usize) -> bool {
1470+ self.language_servers().any(|l| l.id() == id)
14461471 }
14471472
14481473 pub fn diff_handle(&self) -> Option<&DiffHandle> {
@@ -1565,12 +1590,29 @@ impl Document {
15651590 &self.diagnostics
15661591 }
15671592
1568− pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
1569− self.diagnostics = diagnostics;
1593+ pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator {
1594+ self.diagnostics.iter().filter(|d| {
1595+ self.language_servers_with_feature(LanguageServerFeature::Diagnostics)
1596+ .any(|ls| ls.id() == d.language_server_id)
1597+ })
1598+ }
1599+
1600+ pub fn replace_diagnostics(
1601+ &mut self,
1602+ mut diagnostics: Vec<Diagnostic>,
1603+ language_server_id: usize,
1604+ ) {
1605+ self.clear_diagnostics(language_server_id);
1606+ self.diagnostics.append(&mut diagnostics);
15701607 self.diagnostics
15711608 .sort_unstable_by_key(|diagnostic| diagnostic.range);
15721609 }
15731610
1611+ pub fn clear_diagnostics(&mut self, language_server_id: usize) {
1612+ self.diagnostics
1613+ .retain(|d| d.language_server_id != language_server_id);
1614+ }
1615+
15741616 /// Get the document's auto pairs. If the document has a recognized
15751617 /// language config with auto pairs configured, returns that;
15761618 /// otherwise, falls back to the global auto pairs config. If the global
helix-view/src/editor.rs+43−27
@@ -818,7 +818,7 @@ pub struct Editor {
818818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
819819 pub macro_replaying: Vec<char>,
820820 pub language_servers: helix_lsp::Registry,
821− pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
821+ pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
822822 pub diff_providers: DiffProviderRegistry,
823823
824824 pub debugger: Option<dap::Client>,
@@ -874,7 +874,7 @@ pub struct Editor {
874874 /// times during rendering and should not be set by other functions.
875875 pub cursor_cache: Cell<Option<Option<Position>>>,
876876 /// When a new completion request is sent to the server old
877− /// unifinished request must be dropped. Each completion
877+ /// unfinished request must be dropped. Each completion
878878 /// request is associated with a channel that cancels
879879 /// when the channel is dropped. That channel is stored
880880 /// here. When a new completion request is sent this
@@ -941,6 +941,7 @@ impl Editor {
941941 syn_loader: Arc<syntax::Loader>,
942942 config: Arc<dyn DynAccess<Config>>,
943943 ) -> Self {
944+ let language_servers = helix_lsp::Registry::new(syn_loader.clone());
944945 let conf = config.load();
945946 let auto_pairs = (&conf.auto_pairs).into();
946947
@@ -960,7 +961,7 @@ impl Editor {
960961 macro_recording: None,
961962 macro_replaying: Vec::new(),
962963 theme: theme_loader.default(),
963− language_servers: helix_lsp::Registry::new(),
964+ language_servers,
964965 diagnostics: BTreeMap::new(),
965966 diff_providers: DiffProviderRegistry::default(),
966967 debugger: None,
@@ -1092,60 +1093,75 @@ impl Editor {
10921093 self._refresh();
10931094 }
10941095
1096+ #[inline]
1097+ pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
1098+ self.language_servers.get_by_id(language_server_id)
1099+ }
1100+
10951101 /// Refreshes the language server for a given document
1096− pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1097− self.launch_language_server(doc_id)
1102+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1103+ self.launch_language_servers(doc_id)
10981104 }
10991105
11001106 /// Launch a language server for a given document
1101− fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1107+ fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
11021108 if !self.config().lsp.enable {
11031109 return None;
11041110 }
1105−
11061111 // if doc doesn't have a URL it's a scratch buffer, ignore it
1107− let doc = self.document(doc_id)?;
1112+ let doc = self.documents.get_mut(&doc_id)?;
1113+ let doc_url = doc.url()?;
11081114 let (lang, path) = (doc.language.clone(), doc.path().cloned());
11091115 let config = doc.config.load();
11101116 let root_dirs = &config.workspace_lsp_roots;
11111117
1112− // try to find a language server based on the language name
1113− let language_server = lang.as_ref().and_then(|language| {
1118+ // try to find language servers based on the language name
1119+ let language_servers = lang.as_ref().and_then(|language| {
11141120 self.language_servers
11151121 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
11161122 .map_err(|e| {
11171123 log::error!(
1118− "Failed to initialize the LSP for `{}` {{ {} }}",
1124+ "Failed to initialize the language servers for `{}` {{ {} }}",
11191125 language.scope(),
11201126 e
11211127 )
11221128 })
11231129 .ok()
1124− .flatten()
11251130 });
11261131
1127− let doc = self.document_mut(doc_id)?;
1128− let doc_url = doc.url()?;
1132+ if let Some(language_servers) = language_servers {
1133+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
11291134
1130− if let Some(language_server) = language_server {
1131− // only spawn a new lang server if the servers aren't the same
1132− if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
1133− if let Some(language_server) = doc.language_server() {
1134− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1135− }
1135+ // only spawn new language servers if the servers aren't the same
11361136
1137− let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1137+ let doc_language_servers_not_in_registry =
1138+ doc.language_servers.iter().filter(|(name, doc_ls)| {
1139+ language_servers
1140+ .get(*name)
1141+ .map_or(true, |ls| ls.id() != doc_ls.id())
1142+ });
11381143
1144+ for (_, language_server) in doc_language_servers_not_in_registry {
1145+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1146+ }
1147+
1148+ let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
1149+ doc.language_servers
1150+ .get(*name)
1151+ .map_or(true, |doc_ls| ls.id() != doc_ls.id())
1152+ });
1153+
1154+ for (_, language_server) in language_servers_not_in_doc {
11391155 // TODO: this now races with on_init code if the init happens too quickly
11401156 tokio::spawn(language_server.text_document_did_open(
1141− doc_url,
1157+ doc_url.clone(),
11421158 doc.version(),
11431159 doc.text(),
1144− language_id,
1160+ language_id.clone(),
11451161 ));
1146−
1147− doc.set_language_server(Some(language_server));
11481162 }
1163+
1164+ doc.language_servers = language_servers;
11491165 }
11501166 Some(())
11511167 }
@@ -1338,7 +1354,7 @@ impl Editor {
13381354 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
13391355
13401356 let id = self.new_document(doc);
1341− let _ = self.launch_language_server(id);
1357+ let _ = self.launch_language_servers(id);
13421358
13431359 id
13441360 };
@@ -1368,7 +1384,7 @@ impl Editor {
13681384 // This will also disallow any follow-up writes
13691385 self.saves.remove(&doc_id);
13701386
1371− if let Some(language_server) = doc.language_server() {
1387+ for language_server in doc.language_servers() {
13721388 // TODO: track error
13731389 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
13741390 }
helix-view/src/gutter.rs+17−19
@@ -1,5 +1,7 @@
11use std::fmt::Write;
22
3+use helix_core::syntax::LanguageServerFeature;
4+
35use crate::{
46 editor::GutterType,
57 graphics::{Style, UnderlineStyle},
@@ -55,7 +57,7 @@ pub fn diagnostic<'doc>(
5557 let error = theme.get("error");
5658 let info = theme.get("info");
5759 let hint = theme.get("hint");
58− let diagnostics = doc.diagnostics();
60+ let diagnostics = &doc.diagnostics;
5961
6062 Box::new(
6163 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
@@ -63,28 +65,24 @@ pub fn diagnostic<'doc>(
6365 return None;
6466 }
6567 use helix_core::diagnostic::Severity;
66− if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
67− let after = diagnostics[index..].iter().take_while(|d| d.line == line);
68−
69− let before = diagnostics[..index]
70− .iter()
71− .rev()
72− .take_while(|d| d.line == line);
73−
74− let diagnostics_on_line = after.chain(before);
75−
76− // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search.
77− let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap();
78−
79− write!(out, "●").unwrap();
80− return Some(match diagnostic.severity {
68+ let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line);
69+ let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..]
70+ .iter()
71+ .take_while(|d| {
72+ d.line == line
73+ && doc
74+ .language_servers_with_feature(LanguageServerFeature::Diagnostics)
75+ .any(|ls| ls.id() == d.language_server_id)
76+ });
77+ diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
78+ write!(out, "●").ok();
79+ match d.severity {
8180 Some(Severity::Error) => error,
8281 Some(Severity::Warning) | None => warning,
8382 Some(Severity::Info) => info,
8483 Some(Severity::Hint) => hint,
85− });
86− }
87− None
84+ }
85+ })
8886 },
8987 )
9088}
languages.toml+235−180
@@ -1,6 +1,143 @@
11# Language support configuration.
22# See the languages documentation: https://docs.helix-editor.com/master/languages.html
33
4+[language-server]
5+
6+awk-language-server = { command = "awk-language-server" }
7+bash-language-server = { command = "bash-language-server", args = ["start"] }
8+bass = { command = "bass", args = ["--lsp"] }
9+bicep-langserver = { command = "bicep-langserver" }
10+cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
11+clangd = { command = "clangd" }
12+clojure-lsp = { command = "clojure-lsp" }
13+cmake-language-server = { command = "cmake-language-server" }
14+crystalline = { command = "crystalline", args = ["--stdio"] }
15+cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
16+cuelsp = { command = "cuelsp" }
17+dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
18+dhall-lsp-server = { command = "dhall-lsp-server" }
19+docker-langserver = { command = "docker-langserver", args = ["--stdio"] }
20+dot-language-server = { command = "dot-language-server", args = ["--stdio"] }
21+elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } }
22+elm-language-server = { command = "elm-language-server" }
23+elvish = { command = "elvish", args = ["-lsp"] }
24+erlang-ls = { command = "erlang_ls" }
25+forc = { command = "forc", args = ["lsp"] }
26+fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
27+gleam = { command = "gleam", args = ["lsp"] }
28+haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
29+idris2-lsp = { command = "idris2-lsp" }
30+intelephense = { command = "intelephense", args = ["--stdio"] }
31+jdtls = { command = "jdtls" }
32+jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
33+julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] }
34+kotlin-language-server = { command = "kotlin-language-server" }
35+lean = { command = "lean", args = [ "--server" ] }
36+markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
37+marksman = { command = "marksman", args = ["server"] }
38+metals = { command = "metals", config = { "isHttpEnabled" = true } }
39+mint = { command = "mint", args = ["ls"] }
40+nil = { command = "nil" }
41+nimlangserver = { command = "nimlangserver" }
42+nls = { command = "nls" }
43+ocamllsp = { command = "ocamllsp" }
44+ols = { command = "ols", args = [] }
45+omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
46+openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
47+pasls = { command = "pasls", args = [] }
48+perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
49+prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
50+purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
51+pylsp = { command = "pylsp" }
52+qmlls = { command = "qmlls" }
53+r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
54+racket = { command = "racket", args = ["-l", "racket-langserver"] }
55+regols = { command = "regols" }
56+rescript-language-server = { command = "rescript-language-server", args = ["--stdio"] }
57+robotframework_ls = { command = "robotframework_ls" }
58+serve-d = { command = "serve-d" }
59+slint-lsp = { command = "slint-lsp", args = [] }
60+solargraph = { command = "solargraph", args = ["stdio"] }
61+solc = { command = "solc", args = ["--lsp"] }
62+sourcekit-lsp = { command = "sourcekit-lsp" }
63+svelteserver = { command = "svelteserver", args = ["--stdio"] }
64+svlangserver = { command = "svlangserver", args = [] }
65+swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
66+taplo = { command = "taplo", args = ["lsp", "stdio"] }
67+terraform-ls = { command = "terraform-ls", args = ["serve"] }
68+texlab = { command = "texlab" }
69+vala-language-server = { command = "vala-language-server" }
70+vhdl_ls = { command = "vhdl_ls", args = [] }
71+vlang-language-server = { command = "v", args = ["ls"] }
72+vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { "provideFormatter" = true }}
73+vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
74+vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } }
75+vuels = { command = "vls" }
76+wgsl_analyzer = { command = "wgsl_analyzer" }
77+yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
78+zls = { command = "zls" }
79+
80+
81+[language-server.lua-language-server]
82+command = "lua-language-server"
83+
84+[language-server.lua-language-server.config.Lua.hint]
85+enable = true
86+arrayIndex = "Enable"
87+setType = true
88+paramName = "All"
89+paramType = true
90+await = true
91+
92+
93+[language-server.gopls]
94+command = "gopls"
95+
96+[language-server.gopls.config.hints]
97+assignVariableTypes = true
98+compositeLiteralFields = true
99+constantValues = true
100+functionTypeParameters = true
101+parameterNames = true
102+rangeVariableTypes = true
103+
104+
105+[language-server.rust-analyzer]
106+command = "rust-analyzer"
107+
108+[language-server.rust-analyzer.config]
109+inlayHints.bindingModeHints.enable = false
110+inlayHints.closingBraceHints.minLines = 10
111+inlayHints.closureReturnTypeHints.enable = "with_block"
112+inlayHints.discriminantHints.enable = "fieldless"
113+inlayHints.lifetimeElisionHints.enable = "skip_trivial"
114+inlayHints.typeHints.hideClosureInitialization = false
115+
116+
117+[language-server.typescript-language-server]
118+command = "typescript-language-server"
119+args = ["--stdio"]
120+config.hostInfo = "helix"
121+
122+[language-server.typescript-language-server.config.typescript.inlayHints]
123+includeInlayEnumMemberValueHints = true
124+includeInlayFunctionLikeReturnTypeHints = true
125+includeInlayFunctionParameterTypeHints = true
126+includeInlayParameterNameHints = "all"
127+includeInlayParameterNameHintsWhenArgumentMatchesName = true
128+includeInlayPropertyDeclarationTypeHints = true
129+includeInlayVariableTypeHints = true
130+
131+[language-server.typescript-language-server.config.javascript.inlayHints]
132+includeInlayEnumMemberValueHints = true
133+includeInlayFunctionLikeReturnTypeHints = true
134+includeInlayFunctionParameterTypeHints = true
135+includeInlayParameterNameHints = "all"
136+includeInlayParameterNameHintsWhenArgumentMatchesName = true
137+includeInlayPropertyDeclarationTypeHints = true
138+includeInlayVariableTypeHints = true
139+
140+
4141[[language]]
5142name = "rust"
6143scope = "source.rust"
@@ -9,7 +146,7 @@ file-types = ["rs"]
9146roots = ["Cargo.toml", "Cargo.lock"]
10147auto-format = true
11148comment-token = "//"
12−language-server = { command = "rust-analyzer" }
149+language-servers = [ "rust-analyzer" ]
13150indent = { tab-width = 4, unit = " " }
14151
15152[language.auto-pairs]
@@ -19,14 +156,6 @@ indent = { tab-width = 4, unit = " " }
19156'"' = '"'
20157'`' = '`'
21158
22−[language.config]
23−inlayHints.bindingModeHints.enable = false
24−inlayHints.closingBraceHints.minLines = 10
25−inlayHints.closureReturnTypeHints.enable = "with_block"
26−inlayHints.discriminantHints.enable = "fieldless"
27−inlayHints.lifetimeElisionHints.enable = "skip_trivial"
28−inlayHints.typeHints.hideClosureInitialization = false
29−
30159[language.debugger]
31160name = "lldb-vscode"
32161transport = "stdio"
@@ -65,7 +194,7 @@ name = "sway"
65194scope = "source.sway"
66195injection-regex = "sway"
67196file-types = ["sw"]
68−language-server = { command = "forc", args = ["lsp"] }
197+language-servers = [ "forc" ]
69198roots = ["Forc.toml", "Forc.lock"]
70199indent = { tab-width = 4, unit = " " }
71200comment-token = "//"
@@ -81,7 +210,7 @@ injection-regex = "toml"
81210file-types = ["toml", "poetry.lock"]
82211roots = []
83212comment-token = "#"
84−language-server = { command = "taplo", args = ["lsp", "stdio"] }
213+language-servers = [ "taplo" ]
85214indent = { tab-width = 2, unit = " " }
86215
87216[[grammar]]
@@ -95,7 +224,7 @@ injection-regex = "awk"
95224file-types = ["awk", "gawk", "nawk", "mawk"]
96225roots = []
97226comment-token = "#"
98−language-server = { command = "awk-language-server" }
227+language-servers = [ "awk-language-server" ]
99228indent = { tab-width = 2, unit = " " }
100229
101230[[grammar]]
@@ -123,8 +252,7 @@ file-types = ["ex", "exs", "mix.lock"]
123252shebangs = ["elixir"]
124253roots = ["mix.exs", "mix.lock"]
125254comment-token = "#"
126−language-server = { command = "elixir-ls" }
127−config = { elixirLS.dialyzerEnabled = false }
255+language-servers = [ "elixir-ls" ]
128256indent = { tab-width = 2, unit = " " }
129257
130258[[grammar]]
@@ -153,7 +281,7 @@ file-types = ["mint"]
153281shebangs = []
154282roots = []
155283comment-token = "//"
156−language-server = { command = "mint", args = ["ls"] }
284+language-servers = [ "mint" ]
157285indent = { tab-width = 2, unit = " " }
158286
159287[[language]]
@@ -162,9 +290,8 @@ scope = "source.json"
162290injection-regex = "json"
163291file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
164292roots = []
165−language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
293+language-servers = [ "vscode-json-language-server" ]
166294auto-format = true
167−config = { "provideFormatter" = true }
168295indent = { tab-width = 2, unit = " " }
169296
170297[[grammar]]
@@ -178,7 +305,7 @@ injection-regex = "c"
178305file-types = ["c"] # TODO: ["h"]
179306roots = []
180307comment-token = "//"
181−language-server = { command = "clangd" }
308+language-servers = [ "clangd" ]
182309indent = { tab-width = 2, unit = " " }
183310
184311[language.debugger]
@@ -215,7 +342,7 @@ injection-regex = "cpp"
215342file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
216343roots = []
217344comment-token = "//"
218−language-server = { command = "clangd" }
345+language-servers = [ "clangd" ]
219346indent = { tab-width = 2, unit = " " }
220347
221348[language.debugger]
@@ -253,7 +380,7 @@ roots = ["shard.yml", "shard.lock"]
253380comment-token = "#"
254381indent = { tab-width = 2, unit = " " }
255382grammar = "ruby"
256−language-server = { command = "crystalline", args = ["--stdio"] }
383+language-servers = [ "crystalline" ]
257384
258385[[language]]
259386name = "c-sharp"
@@ -263,7 +390,7 @@ file-types = ["cs"]
263390roots = ["sln", "csproj"]
264391comment-token = "//"
265392indent = { tab-width = 4, unit = "\t" }
266−language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
393+language-servers = [ "omnisharp" ]
267394
268395[language.debugger]
269396name = "netcoredbg"
@@ -296,18 +423,10 @@ file-types = ["go"]
296423roots = ["go.work", "go.mod"]
297424auto-format = true
298425comment-token = "//"
299−language-server = { command = "gopls" }
426+language-servers = [ "gopls" ]
300427# TODO: gopls needs utf-8 offsets?
301428indent = { tab-width = 4, unit = "\t" }
302429
303−[language.config.hints]
304−assignVariableTypes = true
305−compositeLiteralFields = true
306−constantValues = true
307−functionTypeParameters = true
308−parameterNames = true
309−rangeVariableTypes = true
310−
311430[language.debugger]
312431name = "go"
313432transport = "tcp"
@@ -351,7 +470,7 @@ file-types = ["go.mod"]
351470roots = []
352471auto-format = true
353472comment-token = "//"
354−language-server = { command = "gopls" }
473+language-servers = [ "gopls" ]
355474indent = { tab-width = 4, unit = "\t" }
356475
357476[[grammar]]
@@ -365,7 +484,7 @@ injection-regex = "gotmpl"
365484file-types = ["gotmpl"]
366485roots = []
367486comment-token = "//"
368−language-server = { command = "gopls" }
487+language-servers = [ "gopls" ]
369488indent = { tab-width = 2, unit = " " }
370489
371490[[grammar]]
@@ -380,7 +499,7 @@ file-types = ["go.work"]
380499roots = []
381500auto-format = true
382501comment-token = "//"
383−language-server = { command = "gopls" }
502+language-servers = [ "gopls" ]
384503indent = { tab-width = 4, unit = "\t" }
385504
386505[[grammar]]
@@ -391,26 +510,15 @@ source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd
391510name = "javascript"
392511scope = "source.js"
393512injection-regex = "(js|javascript)"
513+language-id = "javascript"
394514file-types = ["js", "mjs", "cjs"]
395515shebangs = ["node"]
396516roots = []
397517comment-token = "//"
398518# TODO: highlights-params
399−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
519+language-servers = [ "typescript-language-server" ]
400520indent = { tab-width = 2, unit = " " }
401521
402−[language.config]
403−hostInfo = "helix"
404−
405−[language.config.javascript.inlayHints]
406−includeInlayEnumMemberValueHints = true
407−includeInlayFunctionLikeReturnTypeHints = true
408−includeInlayFunctionParameterTypeHints = true
409−includeInlayParameterNameHints = "all"
410−includeInlayParameterNameHintsWhenArgumentMatchesName = true
411−includeInlayPropertyDeclarationTypeHints = true
412−includeInlayVariableTypeHints = true
413−
414522[language.debugger]
415523name = "node-debug2"
416524transport = "stdio"
@@ -431,48 +539,26 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev =
431539name = "jsx"
432540scope = "source.jsx"
433541injection-regex = "jsx"
542+language-id = "javascriptreact"
434543file-types = ["jsx"]
435544roots = []
436545comment-token = "//"
437−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascriptreact" }
546+language-servers = [ "typescript-language-server" ]
438547indent = { tab-width = 2, unit = " " }
439548grammar = "javascript"
440549
441−[language.config]
442−hostInfo = "helix"
443−
444−[language.config.javascript.inlayHints]
445−includeInlayEnumMemberValueHints = true
446−includeInlayFunctionLikeReturnTypeHints = true
447−includeInlayFunctionParameterTypeHints = true
448−includeInlayParameterNameHints = "all"
449−includeInlayParameterNameHintsWhenArgumentMatchesName = true
450−includeInlayPropertyDeclarationTypeHints = true
451−includeInlayVariableTypeHints = true
452−
453550[[language]]
454551name = "typescript"
455552scope = "source.ts"
456553injection-regex = "(ts|typescript)"
457554file-types = ["ts", "mts", "cts"]
555+language-id = "typescript"
458556shebangs = []
459557roots = []
460558# TODO: highlights-params
461−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
559+language-servers = [ "typescript-language-server" ]
462560indent = { tab-width = 2, unit = " " }
463561
464−[language.config]
465−hostInfo = "helix"
466−
467−[language.config.typescript.inlayHints]
468−includeInlayEnumMemberValueHints = true
469−includeInlayFunctionLikeReturnTypeHints = true
470−includeInlayFunctionParameterTypeHints = true
471−includeInlayParameterNameHints = "all"
472−includeInlayParameterNameHintsWhenArgumentMatchesName = true
473−includeInlayPropertyDeclarationTypeHints = true
474−includeInlayVariableTypeHints = true
475−
476562[[grammar]]
477563name = "typescript"
478564source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
@@ -481,24 +567,13 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev =
481567name = "tsx"
482568scope = "source.tsx"
483569injection-regex = "(tsx)" # |typescript
570+language-id = "typescriptreact"
484571file-types = ["tsx"]
485572roots = []
486573# TODO: highlights-params
487−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
574+language-servers = [ "typescript-language-server" ]
488575indent = { tab-width = 2, unit = " " }
489576
490−[language.config]
491−hostInfo = "helix"
492−
493−[language.config.typescript.inlayHints]
494−includeInlayEnumMemberValueHints = true
495−includeInlayFunctionLikeReturnTypeHints = true
496−includeInlayFunctionParameterTypeHints = true
497−includeInlayParameterNameHints = "all"
498−includeInlayParameterNameHintsWhenArgumentMatchesName = true
499−includeInlayPropertyDeclarationTypeHints = true
500−includeInlayVariableTypeHints = true
501−
502577[[grammar]]
503578name = "tsx"
504579source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@@ -509,9 +584,8 @@ scope = "source.css"
509584injection-regex = "css"
510585file-types = ["css", "scss"]
511586roots = []
512−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
587+language-servers = [ "vscode-css-language-server" ]
513588auto-format = true
514−config = { "provideFormatter" = true }
515589indent = { tab-width = 2, unit = " " }
516590
517591[[grammar]]
@@ -524,9 +598,8 @@ scope = "source.scss"
524598injection-regex = "scss"
525599file-types = ["scss"]
526600roots = []
527−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
601+language-servers = [ "vscode-css-language-server" ]
528602auto-format = true
529−config = { "provideFormatter" = true }
530603indent = { tab-width = 2, unit = " " }
531604
532605[[grammar]]
@@ -539,9 +612,8 @@ scope = "text.html.basic"
539612injection-regex = "html"
540613file-types = ["html"]
541614roots = []
542−language-server = { command = "vscode-html-language-server", args = ["--stdio"] }
615+language-servers = [ "vscode-html-language-server" ]
543616auto-format = true
544−config = { "provideFormatter" = true }
545617indent = { tab-width = 2, unit = " " }
546618
547619[[grammar]]
@@ -556,7 +628,7 @@ file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstr
556628shebangs = ["python"]
557629roots = []
558630comment-token = "#"
559−language-server = { command = "pylsp" }
631+language-servers = [ "pylsp" ]
560632# TODO: pyls needs utf-8 offsets
561633indent = { tab-width = 4, unit = " " }
562634
@@ -572,7 +644,7 @@ file-types = ["ncl"]
572644shebangs = []
573645roots = []
574646comment-token = "#"
575−language-server = { command = "nls" }
647+language-servers = [ "nls" ]
576648indent = { tab-width = 2, unit = " " }
577649
578650[[grammar]]
@@ -587,7 +659,7 @@ file-types = ["nix"]
587659shebangs = []
588660roots = []
589661comment-token = "#"
590−language-server = { command = "nil" }
662+language-servers = [ "nil" ]
591663indent = { tab-width = 2, unit = " " }
592664
593665[[grammar]]
@@ -602,7 +674,7 @@ file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile",
602674shebangs = ["ruby"]
603675roots = []
604676comment-token = "#"
605−language-server = { command = "solargraph", args = ["stdio"] }
677+language-servers = [ "solargraph" ]
606678indent = { tab-width = 2, unit = " " }
607679
608680[[grammar]]
@@ -617,7 +689,7 @@ file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile
617689shebangs = ["sh", "bash", "dash", "zsh"]
618690roots = []
619691comment-token = "#"
620−language-server = { command = "bash-language-server", args = ["start"] }
692+language-servers = [ "bash-language-server" ]
621693indent = { tab-width = 2, unit = " " }
622694
623695[[grammar]]
@@ -631,7 +703,7 @@ injection-regex = "php"
631703file-types = ["php", "inc"]
632704shebangs = ["php"]
633705roots = ["composer.json", "index.php"]
634−language-server = { command = "intelephense", args = ["--stdio"] }
706+language-servers = [ "intelephense" ]
635707indent = { tab-width = 4, unit = " " }
636708
637709[[grammar]]
@@ -657,7 +729,7 @@ injection-regex = "tex"
657729file-types = ["tex", "sty", "cls"]
658730roots = []
659731comment-token = "%"
660−language-server = { command = "texlab" }
732+language-servers = [ "texlab" ]
661733indent = { tab-width = 4, unit = "\t" }
662734
663735[[grammar]]
@@ -671,7 +743,7 @@ injection-regex = "bib"
671743file-types = ["bib"]
672744roots = []
673745comment-token = "%"
674−language-server = { command = "texlab" }
746+language-servers = [ "texlab" ]
675747indent = { tab-width = 4, unit = "\t" }
676748auto-format = true
677749
@@ -699,7 +771,7 @@ injection-regex = "lean"
699771file-types = ["lean"]
700772roots = [ "lakefile.lean" ]
701773comment-token = "--"
702−language-server = { command = "lean", args = [ "--server" ] }
774+language-servers = [ "lean" ]
703775indent = { tab-width = 2, unit = " " }
704776
705777[[grammar]]
@@ -713,13 +785,7 @@ injection-regex = "julia"
713785file-types = ["jl"]
714786roots = ["Manifest.toml", "Project.toml"]
715787comment-token = "#"
716−language-server = { command = "julia", timeout = 60, args = [
717− "--startup-file=no",
718− "--history-file=no",
719− "--quiet",
720− "-e",
721− "using LanguageServer; runserver()",
722− ] }
788+language-servers = [ "julia" ]
723789indent = { tab-width = 4, unit = " " }
724790
725791[[grammar]]
@@ -732,7 +798,7 @@ scope = "source.java"
732798injection-regex = "java"
733799file-types = ["java"]
734800roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
735−language-server = { command = "jdtls" }
801+language-servers = [ "jdtls" ]
736802indent = { tab-width = 4, unit = " " }
737803
738804[[grammar]]
@@ -773,7 +839,7 @@ file-types = ["ml"]
773839shebangs = []
774840roots = []
775841comment-token = "(**)"
776−language-server = { command = "ocamllsp" }
842+language-servers = [ "ocamllsp" ]
777843indent = { tab-width = 2, unit = " " }
778844
779845[language.auto-pairs]
@@ -794,7 +860,7 @@ file-types = ["mli"]
794860shebangs = []
795861roots = []
796862comment-token = "(**)"
797−language-server = { command = "ocamllsp" }
863+language-servers = [ "ocamllsp" ]
798864indent = { tab-width = 2, unit = " " }
799865
800866[language.auto-pairs]
@@ -817,15 +883,7 @@ shebangs = ["lua"]
817883roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
818884comment-token = "--"
819885indent = { tab-width = 2, unit = " " }
820−language-server = { command = "lua-language-server", args = [] }
821−
822−[language.config.Lua.hint]
823−enable = true
824−arrayIndex = "Enable"
825−setType = true
826−paramName = "All"
827−paramType = true
828−await = true
886+language-servers = [ "lua-language-server" ]
829887
830888[[grammar]]
831889name = "lua"
@@ -838,7 +896,7 @@ injection-regex = "svelte"
838896file-types = ["svelte"]
839897roots = []
840898indent = { tab-width = 2, unit = " " }
841−language-server = { command = "svelteserver", args = ["--stdio"] }
899+language-servers = [ "svelteserver" ]
842900
843901[[grammar]]
844902name = "svelte"
@@ -851,7 +909,7 @@ injection-regex = "vue"
851909file-types = ["vue"]
852910roots = ["package.json", "vue.config.js"]
853911indent = { tab-width = 2, unit = " " }
854−language-server = { command = "vls" }
912+language-servers = [ "vuels" ]
855913
856914[[grammar]]
857915name = "vue"
@@ -864,7 +922,7 @@ file-types = ["yml", "yaml"]
864922roots = []
865923comment-token = "#"
866924indent = { tab-width = 2, unit = " " }
867−language-server = { command = "yaml-language-server", args = ["--stdio"] }
925+language-servers = [ "yaml-language-server" ]
868926injection-regex = "yml|yaml"
869927
870928[[grammar]]
@@ -878,7 +936,7 @@ injection-regex = "haskell"
878936file-types = ["hs", "hs-boot"]
879937roots = ["Setup.hs", "stack.yaml", "cabal.project"]
880938comment-token = "--"
881−language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
939+language-servers = [ "haskell-language-server" ]
882940indent = { tab-width = 2, unit = " " }
883941
884942[[grammar]]
@@ -892,7 +950,7 @@ injection-regex = "purescript"
892950file-types = ["purs"]
893951roots = ["spago.dhall", "bower.json"]
894952comment-token = "--"
895−language-server = { command = "purescript-language-server", args = ["--stdio"] }
953+language-servers = [ "purescript-language-server" ]
896954indent = { tab-width = 2, unit = " " }
897955auto-format = true
898956formatter = { command = "purs-tidy", args = ["format"] }
@@ -906,7 +964,7 @@ file-types = ["zig"]
906964roots = ["build.zig"]
907965auto-format = true
908966comment-token = "//"
909−language-server = { command = "zls" }
967+language-servers = [ "zls" ]
910968indent = { tab-width = 4, unit = " " }
911969formatter = { command = "zig" , args = ["fmt", "--stdin"] }
912970
@@ -944,10 +1002,7 @@ roots = []
9441002file-types = ["pl", "prolog"]
9451003shebangs = ["swipl"]
9461004comment-token = "%"
947−language-server = { command = "swipl", args = [
948− "-g", "use_module(library(lsp_server))",
949− "-g", "lsp_server:main",
950− "-t", "halt", "--", "stdio"] }
1005+language-servers = [ "swipl" ]
9511006
9521007[[language]]
9531008name = "tsq"
@@ -969,7 +1024,7 @@ file-types = ["cmake", "CMakeLists.txt"]
9691024roots = []
9701025comment-token = "#"
9711026indent = { tab-width = 2, unit = " " }
972−language-server = { command = "cmake-language-server" }
1027+language-servers = [ "cmake-language-server" ]
9731028injection-regex = "cmake"
9741029
9751030[[grammar]]
@@ -1009,7 +1064,7 @@ file-types = ["pl", "pm", "t"]
10091064shebangs = ["perl"]
10101065roots = []
10111066comment-token = "#"
1012−language-server = { command = "perlnavigator", args= ["--stdio"] }
1067+language-servers = [ "perlnavigator" ]
10131068indent = { tab-width = 2, unit = " " }
10141069
10151070[[grammar]]
@@ -1023,7 +1078,7 @@ roots = []
10231078file-types = ["rkt", "rktd", "rktl", "scrbl"]
10241079shebangs = ["racket"]
10251080comment-token = ";"
1026−language-server = { command = "racket", args = ["-l", "racket-langserver"] }
1081+language-servers = [ "racket" ]
10271082grammar = "scheme"
10281083
10291084[[language]]
@@ -1034,7 +1089,7 @@ file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
10341089shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
10351090comment-token = ";"
10361091indent = { tab-width = 2, unit = " " }
1037−language-server = { command = "cl-lsp", args = [ "stdio" ] }
1092+language-servers = [ "cl-lsp" ]
10381093grammar = "scheme"
10391094
10401095[language.auto-pairs]
@@ -1060,7 +1115,7 @@ scope = "source.wgsl"
10601115file-types = ["wgsl"]
10611116roots = []
10621117comment-token = "//"
1063−language-server = { command = "wgsl_analyzer" }
1118+language-servers = [ "wgsl_analyzer" ]
10641119indent = { tab-width = 4, unit = " " }
10651120
10661121[[grammar]]
@@ -1123,7 +1178,7 @@ scope = "source.md"
11231178injection-regex = "md|markdown"
11241179file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
11251180roots = [".marksman.toml"]
1126−language-server = { command = "marksman", args=["server"] }
1181+language-servers = [ "marksman" ]
11271182indent = { tab-width = 2, unit = " " }
11281183
11291184[[grammar]]
@@ -1149,7 +1204,7 @@ file-types = ["dart"]
11491204roots = ["pubspec.yaml"]
11501205auto-format = true
11511206comment-token = "//"
1152−language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
1207+language-servers = [ "dart" ]
11531208indent = { tab-width = 2, unit = " " }
11541209
11551210[[grammar]]
@@ -1163,8 +1218,7 @@ roots = ["build.sbt", "build.sc", "build.gradle", "build.gradle.kts", "pom.xml",
11631218file-types = ["scala", "sbt", "sc"]
11641219comment-token = "//"
11651220indent = { tab-width = 2, unit = " " }
1166−language-server = { command = "metals" }
1167−config = { "isHttpEnabled" = true }
1221+language-servers = [ "metals" ]
11681222
11691223[[grammar]]
11701224name = "scala"
@@ -1178,7 +1232,7 @@ roots = ["Dockerfile", "Containerfile"]
11781232file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
11791233comment-token = "#"
11801234indent = { tab-width = 2, unit = " " }
1181−language-server = { command = "docker-langserver", args = ["--stdio"] }
1235+language-servers = [ "docker-langserver" ]
11821236
11831237[[grammar]]
11841238name = "dockerfile"
@@ -1218,7 +1272,7 @@ roots = []
12181272file-types = ["git-rebase-todo"]
12191273injection-regex = "git-rebase"
12201274comment-token = "#"
1221−indent = { tab-width = 2, unit = " " }
1275+indent = { tab-width = 2, unit = "y" }
12221276
12231277[[grammar]]
12241278name = "git-rebase"
@@ -1294,7 +1348,7 @@ file-types = ["elm"]
12941348roots = ["elm.json"]
12951349auto-format = true
12961350comment-token = "--"
1297−language-server = { command = "elm-language-server" }
1351+language-servers = [ "elm-language-server" ]
12981352indent = { tab-width = 4, unit = " " }
12991353
13001354[[grammar]]
@@ -1320,7 +1374,7 @@ file-types = ["res"]
13201374roots = ["bsconfig.json"]
13211375auto-format = true
13221376comment-token = "//"
1323−language-server = { command = "rescript-language-server", args = ["--stdio"] }
1377+language-servers = [ "rescript-language-server" ]
13241378indent = { tab-width = 2, unit = " " }
13251379
13261380[[grammar]]
@@ -1336,7 +1390,7 @@ roots = ["rebar.config"]
13361390shebangs = ["escript"]
13371391comment-token = "%%"
13381392indent = { tab-width = 4, unit = " " }
1339−language-server = { command = "erlang_ls" }
1393+language-servers = [ "erlang-ls" ]
13401394
13411395[language.auto-pairs]
13421396'(' = ')'
@@ -1357,7 +1411,7 @@ file-types = ["kt", "kts"]
13571411roots = ["settings.gradle", "settings.gradle.kts"]
13581412comment-token = "//"
13591413indent = { tab-width = 4, unit = " " }
1360−language-server = { command = "kotlin-language-server" }
1414+language-servers = [ "kotlin-language-server" ]
13611415
13621416[[grammar]]
13631417name = "kotlin"
@@ -1367,11 +1421,12 @@ source = { git = "https://github.com/fwcd/tree-sitter-kotlin", rev = "a4f71eb9b8
13671421name = "hcl"
13681422scope = "source.hcl"
13691423injection-regex = "(hcl|tf|nomad)"
1424+language-id = "terraform"
13701425file-types = ["hcl", "tf", "nomad"]
13711426roots = []
13721427comment-token = "#"
13731428indent = { tab-width = 2, unit = " " }
1374−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform" }
1429+language-servers = [ "terraform-ls" ]
13751430auto-format = true
13761431
13771432[[grammar]]
@@ -1381,11 +1436,12 @@ source = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "3cb7
13811436[[language]]
13821437name = "tfvars"
13831438scope = "source.tfvars"
1439+language-id = "terraform-vars"
13841440file-types = ["tfvars"]
13851441roots = []
13861442comment-token = "#"
13871443indent = { tab-width = 2, unit = " " }
1388−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform-vars" }
1444+language-servers = [ "terraform-ls" ]
13891445auto-format = true
13901446grammar = "hcl"
13911447
@@ -1409,7 +1465,7 @@ file-types = ["sol"]
14091465roots = []
14101466comment-token = "//"
14111467indent = { tab-width = 4, unit = " " }
1412−language-server = { command = "solc", args = ["--lsp"] }
1468+language-servers = [ "solc" ]
14131469
14141470[[grammar]]
14151471name = "solidity"
@@ -1423,7 +1479,7 @@ file-types = ["gleam"]
14231479roots = ["gleam.toml"]
14241480comment-token = "//"
14251481indent = { tab-width = 2, unit = " " }
1426−language-server = { command = "gleam", args = ["lsp"] }
1482+language-servers = [ "gleam" ]
14271483
14281484[[grammar]]
14291485name = "gleam"
@@ -1447,7 +1503,7 @@ file-types = ["robot", "resource"]
14471503comment-token = "#"
14481504roots = []
14491505indent = { tab-width = 4, unit = " " }
1450−language-server = { command = "robotframework_ls" }
1506+language-servers = [ "robotframework_ls" ]
14511507
14521508[[grammar]]
14531509name = "robot"
@@ -1462,7 +1518,7 @@ shebangs = ["r", "R"]
14621518roots = []
14631519comment-token = "#"
14641520indent = { tab-width = 2, unit = " " }
1465−language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
1521+language-servers = [ "r" ]
14661522
14671523[[grammar]]
14681524name = "r"
@@ -1476,7 +1532,7 @@ file-types = ["rmd", "Rmd"]
14761532roots = []
14771533indent = { tab-width = 2, unit = " " }
14781534grammar = "markdown"
1479−language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
1535+language-servers = [ "r" ]
14801536
14811537[[language]]
14821538name = "swift"
@@ -1486,7 +1542,7 @@ file-types = ["swift"]
14861542roots = [ "Package.swift" ]
14871543comment-token = "//"
14881544auto-format = true
1489−language-server = { command = "sourcekit-lsp" }
1545+language-servers = [ "sourcekit-lsp" ]
14901546
14911547[[grammar]]
14921548name = "swift"
@@ -1533,8 +1589,7 @@ injection-regex = "heex"
15331589file-types = ["heex"]
15341590roots = ["mix.exs", "mix.lock"]
15351591indent = { tab-width = 2, unit = " " }
1536−language-server = { command = "elixir-ls" }
1537−config = { elixirLS.dialyzerEnabled = false }
1592+language-servers = [ "elixir-ls" ]
15381593
15391594[[grammar]]
15401595name = "heex"
@@ -1605,7 +1660,7 @@ file-types = ["vala", "vapi"]
16051660roots = []
16061661comment-token = "//"
16071662indent = { tab-width = 2, unit = " " }
1608−language-server = { command = "vala-language-server" }
1663+language-servers = [ "vala-language-server" ]
16091664
16101665[[grammar]]
16111666name = "vala"
@@ -1670,7 +1725,7 @@ auto-format = false
16701725scope = "source.odin"
16711726file-types = ["odin"]
16721727roots = ["ols.json"]
1673−language-server = { command = "ols", args = [] }
1728+language-servers = [ "ols" ]
16741729comment-token = "//"
16751730indent = { tab-width = 4, unit = "\t" }
16761731
@@ -1721,7 +1776,7 @@ scope = "source.v"
17211776file-types = ["v", "vv", "vsh"]
17221777shebangs = ["v run"]
17231778roots = ["v.mod"]
1724−language-server = { command = "v", args = ["ls"] }
1779+language-servers = [ "vlang-language-server" ]
17251780auto-format = true
17261781comment-token = "//"
17271782indent = { tab-width = 4, unit = "\t" }
@@ -1736,7 +1791,7 @@ scope = "source.verilog"
17361791file-types = ["v", "vh", "sv", "svh"]
17371792roots = []
17381793comment-token = "//"
1739−language-server = { command = "svlangserver", args = [] }
1794+language-servers = [ "svlangserver" ]
17401795indent = { tab-width = 2, unit = " " }
17411796injection-regex = "verilog"
17421797
@@ -1775,7 +1830,7 @@ injection-regex = "openscad"
17751830file-types = ["scad"]
17761831roots = []
17771832comment-token = "//"
1778−language-server = { command = "openscad-lsp", args = ["--stdio"] }
1833+language-servers = [ "openscad-lsp" ]
17791834indent = { tab-width = 2, unit = "\t" }
17801835
17811836[[grammar]]
@@ -1789,7 +1844,7 @@ injection-regex = "prisma"
17891844file-types = ["prisma"]
17901845roots = ["package.json"]
17911846comment-token = "//"
1792−language-server = { command = "prisma-language-server", args = ["--stdio"] }
1847+language-servers = [ "prisma-language-server" ]
17931848indent = { tab-width = 2, unit = " " }
17941849
17951850[[grammar]]
@@ -1803,7 +1858,7 @@ injection-regex = "(clojure|clj|edn|boot)"
18031858file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
18041859roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
18051860comment-token = ";"
1806−language-server = { command = "clojure-lsp" }
1861+language-servers = [ "clojure-lsp" ]
18071862indent = { tab-width = 2, unit = " " }
18081863
18091864[[grammar]]
@@ -1827,7 +1882,7 @@ file-types = ["elv"]
18271882roots = []
18281883comment-token = "#"
18291884indent = { tab-width = 2, unit = " " }
1830−language-server = { command = "elvish", args = ["-lsp"] }
1885+language-servers = [ "elvish" ]
18311886grammar = "elvish"
18321887
18331888[[grammar]]
@@ -1843,7 +1898,7 @@ shebangs = []
18431898roots = []
18441899comment-token = "--"
18451900indent = { tab-width = 2, unit = " " }
1846−language-server = { command = "idris2-lsp" }
1901+language-servers = [ "idris2-lsp" ]
18471902
18481903[[language]]
18491904name = "fortran"
@@ -1853,7 +1908,7 @@ file-types = ["f", "for", "f90", "f95", "f03"]
18531908roots = ["fpm.toml"]
18541909comment-token = "!"
18551910indent = { tab-width = 4, unit = " "}
1856−language-server = { command = "fortls", args = ["--lowercase_intrinsics"] }
1911+language-servers = [ "fortls" ]
18571912
18581913[[grammar]]
18591914name = "fortran"
@@ -1880,7 +1935,7 @@ file-types = ["dot"]
18801935roots = []
18811936comment-token = "//"
18821937indent = { tab-width = 4, unit = " " }
1883−language-server = { command = "dot-language-server", args = ["--stdio"] }
1938+language-servers = [ "dot-language-server" ]
18841939
18851940[[grammar]]
18861941name = "dot"
@@ -1894,7 +1949,7 @@ file-types = ["cue"]
18941949roots = ["cue.mod"]
18951950auto-format = true
18961951comment-token = "//"
1897−language-server = { command = "cuelsp" }
1952+language-servers = [ "cuelsp" ]
18981953indent = { tab-width = 4, unit = "\t" }
18991954formatter = { command = "cue", args = ["fmt", "-"] }
19001955
@@ -1910,7 +1965,7 @@ file-types = ["slint"]
19101965roots = []
19111966comment-token = "//"
19121967indent = { tab-width = 4, unit = " " }
1913−language-server = { command = "slint-lsp", args = [] }
1968+language-servers = [ "slint-lsp" ]
19141969
19151970[[grammar]]
19161971name = "slint"
@@ -1962,7 +2017,7 @@ file-types = ["pas", "pp", "inc", "lpr", "lfm"]
19622017roots = []
19632018comment-token = "//"
19642019indent = { tab-width = 2, unit = " " }
1965−language-server = { command = "pasls", args = [] }
2020+language-servers = [ "pasls" ]
19662021
19672022[[grammar]]
19682023name = "pascal"
@@ -1987,7 +2042,7 @@ file-types = ["libsonnet", "jsonnet"]
19872042roots = ["jsonnetfile.json"]
19882043comment-token = "//"
19892044indent = { tab-width = 2, unit = " " }
1990−language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
2045+language-servers = [ "jsonnet-language-server" ]
19912046
19922047[[grammar]]
19932048name = "jsonnet"
@@ -2013,7 +2068,7 @@ file-types = ["bass"]
20132068roots = []
20142069comment-token = ";"
20152070indent = { tab-width = 2, unit = " " }
2016−language-server = { command = "bass", args = ["--lsp"] }
2071+language-servers = [ "bass" ]
20172072
20182073[[grammar]]
20192074name = "bass"
@@ -2049,7 +2104,7 @@ roots = []
20492104comment-token = "//"
20502105injection-regex = "d"
20512106indent = { tab-width = 4, unit = " "}
2052−language-server = { command = "serve-d" }
2107+language-servers = [ "serve-d" ]
20532108formatter = { command = "dfmt" }
20542109
20552110[[grammar]]
@@ -2173,7 +2228,7 @@ roots = []
21732228auto-format = true
21742229comment-token = "//"
21752230indent = { tab-width = 2, unit = " "}
2176−language-server = { command = "bicep-langserver" }
2231+language-servers = [ "bicep-langserver" ]
21772232
21782233[[grammar]]
21792234name = "bicep"
@@ -2184,7 +2239,7 @@ name = "qml"
21842239scope = "source.qml"
21852240file-types = ["qml"]
21862241roots = []
2187−language-server = { command = "qmlls" }
2242+language-servers = [ "qmlls" ]
21882243indent = { tab-width = 4, unit = " " }
21892244grammar = "qmljs"
21902245
@@ -2239,7 +2294,7 @@ file-types = ["dhall"]
22392294roots = []
22402295comment-token = "--"
22412296indent = { tab-width = 2, unit = " " }
2242−language-server = { command = "dhall-lsp-server" }
2297+language-servers = [ "dhall-lsp-server" ]
22432298formatter = { command = "dhall" , args = ["format"] }
22442299
22452300[[grammar]]
@@ -2401,7 +2456,7 @@ file-types = ["smithy"]
24012456roots = ["smithy-build.json"]
24022457comment-token = "//"
24032458indent = { tab-width = 4, unit = " " }
2404−language-server = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
2459+language-servers = [ "cs" ]
24052460
24062461[[grammar]]
24072462name = "smithy"
@@ -2413,7 +2468,7 @@ scope = "source.vhdl"
24132468file-types = ["vhd", "vhdl"]
24142469roots = []
24152470comment-token = "--"
2416−language-server = { command = "vhdl_ls", args = [] }
2471+language-servers = [ "vhdl_ls" ]
24172472indent = { tab-width = 2, unit = " " }
24182473injection-regex = "vhdl"
24192474
@@ -2429,7 +2484,7 @@ injection-regex = "rego"
24292484file-types = ["rego"]
24302485auto-format = true
24312486comment-token = "#"
2432−language-server = { command = "regols" }
2487+language-servers = [ "regols" ]
24332488grammar = "rego"
24342489
24352490[[grammar]]
@@ -2445,7 +2500,7 @@ shebangs = []
24452500roots = []
24462501comment-token = "#"
24472502indent = { tab-width = 2, unit = " " }
2448−language-server = { command = "nimlangserver" }
2503+language-servers = [ "nimlangserver" ]
24492504
24502505[language.auto-pairs]
24512506'(' = ')'
@@ -2485,7 +2540,7 @@ name = "markdoc"
24852540scope = "text.markdoc"
24862541roots = []
24872542file-types = ["mdoc"]
2488−language-server = { command = "markdoc-ls", args = ["--stdio"] }
2543+language-servers = [ "markdoc-ls" ]
24892544
24902545[[grammar]]
24912546name = "markdoc"
@@ -2498,7 +2553,7 @@ injection-regex = "(cl|opencl)"
24982553file-types = ["cl"]
24992554roots = []
25002555comment-token = "//"
2501−language-server = { command = "clangd" }
2556+language-servers = [ "clangd" ]
25022557
25032558[[grammar]]
25042559name = "opencl"
xtask/src/docgen.rs+6−5
@@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
9696 );
9797 }
9898 row.push(
99− lc.language_server
100− .as_ref()
101− .map(|s| s.command.clone())
102− .map(|c| md_mono(&c))
103− .unwrap_or_default(),
99+ lc.language_servers
100+ .iter()
101+ .filter_map(|ls| config.language_server.get(&ls.name))
102+ .map(|s| md_mono(&s.command.clone()))
103+ .collect::<Vec<_>>()
104+ .join(", "),
104105 );
105106
106107 md.push_str(&md_table_row(&row));