on master help sign in
Merge pull request #2507 from Philipp-M/multiple-language-servers
53f47bcBlaž Hrastnik
book/src/generated/typable-cmd.md+2−2
@@ -50,8 +50,8 @@
50| `:reload-all` | Discard changes and reload all documents from the source files. |
50| `:reload-all` | Discard changes and reload all documents from the source files. |
51| `:update`, `:u` | Write changes only if the file has been modified. |
51| `:update`, `:u` | Write changes only if the file has been modified. |
52| `:lsp-workspace-command` | Open workspace command picker |
52| `:lsp-workspace-command` | Open workspace command picker |
53−| `:lsp-restart` | Restarts 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 Server that is in use by the current doc |
54+| `:lsp-stop` | Stops the language servers that are used by the current doc |
55| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
55| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
56| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
56| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
57| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
57| `: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.
9 necessary configuration for the new language. For more information on
9 necessary configuration for the new language. For more information on
10 language configuration, refer to the
10 language configuration, refer to the
11 [language configuration section](../languages.md) of the documentation.
11 [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.
122. If you are adding a new language or updating an existing language server
132. If you are adding a new language or updating an existing language server
13 configuration, run the command `cargo xtask docgen` to update the
14 configuration, run the command `cargo xtask docgen` to update the
14 [Language Support](../lang-support.md) documentation.
15 [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:
18```toml
18```toml
19# in <config_dir>/helix/languages.toml
19# in <config_dir>/helix/languages.toml
20
20
21+[language-server.mylang-lsp]
22+command = "mylang-lsp"
23+
21[[language]]
24[[language]]
22name = "rust"
25name = "rust"
23auto-format = false
26auto-format = false
@@ -41,8 +44,8 @@ injection-regex = "mylang"
41file-types = ["mylang", "myl"]
44file-types = ["mylang", "myl"]
42comment-token = "#"
45comment-token = "#"
43indent = { tab-width = 2, unit = " " }
46indent = { tab-width = 2, unit = " " }
44−language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
45formatter = { command = "mylang-formatter" , args = ["--stdin"] }
47formatter = { command = "mylang-formatter" , args = ["--stdin"] }
48+language-servers = [ "mylang-lsp" ]
46```
49```
47
50
48These configuration keys are available:
51These configuration keys are available:
@@ -50,6 +53,7 @@ These configuration keys are available:
50| Key | Description |
53| Key | Description |
51| ---- | ----------- |
54| ---- | ----------- |
52| `name` | The name of the language |
55| `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 |
53| `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 |
57| `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 |
54| `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. |
58| `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. |
55| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
59| `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:
59| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
63| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
60| `comment-token` | The token to use as a comment-token |
64| `comment-token` | The token to use as a comment-token |
61| `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) |
65| `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) |
63| `config` | Language Server configuration |
67| `config` | Language Server configuration |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
68| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
65| `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 |
69| `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:
92 replaced at runtime with the appropriate path separator for the operating
96 replaced at runtime with the appropriate path separator for the operating
93 system, so this rule would match against `.git\config` files on Windows.
97 system, so this rule would match against `.git\config` files on Windows.
94
98
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`
96
102
97−The `language-server` field takes the following keys:
103+For example:
98
104
99−| Key | Description |
105+```toml
100−| --- | ----------- |
106+[language-server.mylang-lsp]
101−| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
107+command = "mylang-lsp"
102−| `args` | A list of arguments to pass to the language server binary |
108+args = ["--stdio"]
103−| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
109+config = { provideFormatter = true }
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 |
110+environment = { "ENV1" = "value1", "ENV2" = "value2" }
105−| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "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+```
106
119
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 |
107−The top-level `config` field is used to configure the LSP initialization options. A `format`
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+
108−sub-table within `config` can be used to pass extra formatting options to
130+A `format` 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).
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).
110For example with typescript:
132For example with typescript:
111
133
112```toml
134```toml
113−[[language]]
135+[language-server.typescript-language-server]
114−name = "typescript"
115−auto-format = true
116# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
136# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
117config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
137config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
118```
138```
119
139
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+
120## Tree-sitter grammar configuration
195## Tree-sitter grammar configuration
121
196
122The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
197The 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 {
43 pub message: String,
43 pub message: String,
44 pub severity: Option<Severity>,
44 pub severity: Option<Severity>,
45 pub code: Option<NumberOrString>,
45 pub code: Option<NumberOrString>,
46+ pub language_server_id: usize,
46 pub tags: Vec<DiagnosticTag>,
47 pub tags: Vec<DiagnosticTag>,
47 pub source: Option<String>,
48 pub source: Option<String>,
48 pub data: Option<serde_json::Value>,
49 pub data: Option<serde_json::Value>,
helix-core/src/syntax.rs+165−13
@@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
16use std::{
16use std::{
17 borrow::Cow,
17 borrow::Cow,
18 cell::RefCell,
18 cell::RefCell,
19− collections::{HashMap, VecDeque},
19+ collections::{HashMap, HashSet, VecDeque},
20− fmt,
20+ fmt::{self, Display},
21 hash::{Hash, Hasher},
21 hash::{Hash, Hasher},
22 mem::{replace, transmute},
22 mem::{replace, transmute},
23 path::{Path, PathBuf},
23 path::{Path, PathBuf},
@@ -26,7 +26,7 @@ use std::{
26};
26};
27
27
28use once_cell::sync::{Lazy, OnceCell};
28use once_cell::sync::{Lazy, OnceCell};
29−use serde::{Deserialize, Serialize};
29+use serde::{ser::SerializeSeq, Deserialize, Serialize};
30
30
31use helix_loader::grammar::{get_language, load_runtime_file};
31use helix_loader::grammar::{get_language, load_runtime_file};
32
32
@@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
60}
60}
61
61
62#[derive(Debug, Serialize, Deserialize)]
62#[derive(Debug, Serialize, Deserialize)]
63+#[serde(rename_all = "kebab-case")]
63pub struct Configuration {
64pub struct Configuration {
64 pub language: Vec<LanguageConfiguration>,
65 pub language: Vec<LanguageConfiguration>,
66+ #[serde(default)]
67+ pub language_server: HashMap<String, LanguageServerConfiguration>,
65}
68}
66
69
67impl Default for Configuration {
70impl Default for Configuration {
@@ -75,7 +78,10 @@ impl Default for Configuration {
75#[serde(rename_all = "kebab-case", deny_unknown_fields)]
78#[serde(rename_all = "kebab-case", deny_unknown_fields)]
76pub struct LanguageConfiguration {
79pub struct LanguageConfiguration {
77 #[serde(rename = "name")]
80 #[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
79 pub scope: String, // source.rust
85 pub scope: String, // source.rust
80 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
86 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
81 #[serde(default)]
87 #[serde(default)]
@@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
85 pub text_width: Option<usize>,
91 pub text_width: Option<usize>,
86 pub soft_wrap: Option<SoftWrap>,
92 pub soft_wrap: Option<SoftWrap>,
87
93
88− #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
89− pub config: Option<serde_json::Value>,
90−
91 #[serde(default)]
94 #[serde(default)]
92 pub auto_format: bool,
95 pub auto_format: bool,
93
96
@@ -107,8 +110,13 @@ pub struct LanguageConfiguration {
107 #[serde(skip)]
110 #[serde(skip)]
108 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
111 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
109 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
112 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
113+ #[serde(
114+ default,
110− #[serde(skip_serializing_if = "Option::is_none")]
115+ skip_serializing_if = "Vec::is_empty",
111− pub language_server: Option<LanguageServerConfiguration>,
116+ serialize_with = "serialize_lang_features",
117+ deserialize_with = "deserialize_lang_features"
118+ )]
119+ pub language_servers: Vec<LanguageServerFeatures>,
112 #[serde(skip_serializing_if = "Option::is_none")]
120 #[serde(skip_serializing_if = "Option::is_none")]
113 pub indent: Option<IndentationConfiguration>,
121 pub indent: Option<IndentationConfiguration>,
114
122
@@ -208,6 +216,133 @@ impl<'de> Deserialize<'de> for FileType {
208 }
216 }
209}
217}
210
218
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+
211#[derive(Debug, Serialize, Deserialize)]
346#[derive(Debug, Serialize, Deserialize)]
212#[serde(rename_all = "kebab-case")]
347#[serde(rename_all = "kebab-case")]
213pub struct LanguageServerConfiguration {
348pub struct LanguageServerConfiguration {
@@ -217,9 +352,10 @@ pub struct LanguageServerConfiguration {
217 pub args: Vec<String>,
352 pub args: Vec<String>,
218 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
353 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
219 pub environment: HashMap<String, String>,
354 pub environment: HashMap<String, String>,
355+ #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
356+ pub config: Option<serde_json::Value>,
220 #[serde(default = "default_timeout")]
357 #[serde(default = "default_timeout")]
221 pub timeout: u64,
358 pub timeout: u64,
222− pub language_id: Option<String>,
223}
359}
224
360
225#[derive(Debug, Clone, Serialize, Deserialize)]
361#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -594,6 +730,8 @@ pub struct Loader {
594 language_config_ids_by_suffix: HashMap<String, usize>,
730 language_config_ids_by_suffix: HashMap<String, usize>,
595 language_config_ids_by_shebang: HashMap<String, usize>,
731 language_config_ids_by_shebang: HashMap<String, usize>,
596
732
733+ language_server_configs: HashMap<String, LanguageServerConfiguration>,
734+
597 scopes: ArcSwap<Vec<String>>,
735 scopes: ArcSwap<Vec<String>>,
598}
736}
599
737
@@ -601,6 +739,7 @@ impl Loader {
601 pub fn new(config: Configuration) -> Self {
739 pub fn new(config: Configuration) -> Self {
602 let mut loader = Self {
740 let mut loader = Self {
603 language_configs: Vec::new(),
741 language_configs: Vec::new(),
742+ language_server_configs: config.language_server,
604 language_config_ids_by_extension: HashMap::new(),
743 language_config_ids_by_extension: HashMap::new(),
605 language_config_ids_by_suffix: HashMap::new(),
744 language_config_ids_by_suffix: HashMap::new(),
606 language_config_ids_by_shebang: HashMap::new(),
745 language_config_ids_by_shebang: HashMap::new(),
@@ -725,6 +864,10 @@ impl Loader {
725 self.language_configs.iter()
864 self.language_configs.iter()
726 }
865 }
727
866
867+ pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
868+ &self.language_server_configs
869+ }
870+
728 pub fn set_scopes(&self, scopes: Vec<String>) {
871 pub fn set_scopes(&self, scopes: Vec<String>) {
729 self.scopes.store(Arc::new(scopes));
872 self.scopes.store(Arc::new(scopes));
730
873
@@ -2370,7 +2513,10 @@ mod test {
2370 "#,
2513 "#,
2371 );
2514 );
2372
2515
2373− let loader = Loader::new(Configuration { language: vec![] });
2516+ let loader = Loader::new(Configuration {
2517+ language: vec![],
2518+ language_server: HashMap::new(),
2519+ });
2374 let language = get_language("rust").unwrap();
2520 let language = get_language("rust").unwrap();
2375
2521
2376 let query = Query::new(language, query_str).unwrap();
2522 let query = Query::new(language, query_str).unwrap();
@@ -2429,7 +2575,10 @@ mod test {
2429 .map(String::from)
2575 .map(String::from)
2430 .collect();
2576 .collect();
2431
2577
2432− let loader = Loader::new(Configuration { language: vec![] });
2578+ let loader = Loader::new(Configuration {
2579+ language: vec![],
2580+ language_server: HashMap::new(),
2581+ });
2433
2582
2434 let language = get_language("rust").unwrap();
2583 let language = get_language("rust").unwrap();
2435 let config = HighlightConfiguration::new(
2584 let config = HighlightConfiguration::new(
@@ -2532,7 +2681,10 @@ mod test {
2532 ) {
2681 ) {
2533 let source = Rope::from_str(source);
2682 let source = Rope::from_str(source);
2534
2683
2535− let loader = Loader::new(Configuration { language: vec![] });
2684+ let loader = Loader::new(Configuration {
2685+ language: vec![],
2686+ language_server: HashMap::new(),
2687+ });
2536 let language = get_language(language_name).unwrap();
2688 let language = get_language(language_name).unwrap();
2537
2689
2538 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2690 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
helix-lsp/src/client.rs+92−13
@@ -4,7 +4,7 @@ use crate::{
4 Call, Error, OffsetEncoding, Result,
4 Call, Error, OffsetEncoding, Result,
5};
5};
6
6
7−use helix_core::{find_workspace, path, ChangeSet, Rope};
7+use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
9use lsp::{
9use lsp::{
10 notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
10 notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
44#[derive(Debug)]
44#[derive(Debug)]
45pub struct Client {
45pub struct Client {
46 id: usize,
46 id: usize,
47+ name: String,
47 _process: Child,
48 _process: Child,
48 server_tx: UnboundedSender<Payload>,
49 server_tx: UnboundedSender<Payload>,
49 request_counter: AtomicU64,
50 request_counter: AtomicU64,
@@ -166,8 +167,7 @@ impl Client {
166 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167 }
168 }
168
169
169− #[allow(clippy::type_complexity)]
170+ #[allow(clippy::type_complexity, clippy::too_many_arguments)]
170− #[allow(clippy::too_many_arguments)]
171 pub fn start(
171 pub fn start(
172 cmd: &str,
172 cmd: &str,
173 args: &[String],
173 args: &[String],
@@ -176,6 +176,7 @@ impl Client {
176 root_markers: &[String],
176 root_markers: &[String],
177 manual_roots: &[PathBuf],
177 manual_roots: &[PathBuf],
178 id: usize,
178 id: usize,
179+ name: String,
179 req_timeout: u64,
180 req_timeout: u64,
180 doc_path: Option<&std::path::PathBuf>,
181 doc_path: Option<&std::path::PathBuf>,
181 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
182 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@@ -200,7 +201,7 @@ impl Client {
200 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201
202
202 let (server_rx, server_tx, initialize_notify) =
203 let (server_rx, server_tx, initialize_notify) =
203− Transport::start(reader, writer, stderr, id);
204+ Transport::start(reader, writer, stderr, id, name.clone());
204 let (workspace, workspace_is_cwd) = find_workspace();
205 let (workspace, workspace_is_cwd) = find_workspace();
205 let workspace = path::get_normalized_path(&workspace);
206 let workspace = path::get_normalized_path(&workspace);
206 let root = find_lsp_workspace(
207 let root = find_lsp_workspace(
@@ -225,6 +226,7 @@ impl Client {
225
226
226 let client = Self {
227 let client = Self {
227 id,
228 id,
229+ name,
228 _process: process,
230 _process: process,
229 server_tx,
231 server_tx,
230 request_counter: AtomicU64::new(0),
232 request_counter: AtomicU64::new(0),
@@ -240,6 +242,10 @@ impl Client {
240 Ok((client, server_rx, initialize_notify))
242 Ok((client, server_rx, initialize_notify))
241 }
243 }
242
244
245+ pub fn name(&self) -> &str {
246+ &self.name
247+ }
248+
243 pub fn id(&self) -> usize {
249 pub fn id(&self) -> usize {
244 self.id
250 self.id
245 }
251 }
@@ -270,6 +276,87 @@ impl Client {
270 .expect("language server not yet initialized!")
276 .expect("language server not yet initialized!")
271 }
277 }
272
278
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+
273 pub fn offset_encoding(&self) -> OffsetEncoding {
360 pub fn offset_encoding(&self) -> OffsetEncoding {
274 self.capabilities()
361 self.capabilities()
275 .position_encoding
362 .position_encoding
@@ -1295,21 +1382,13 @@ impl Client {
1295 Some(self.call::<lsp::request::CodeActionRequest>(params))
1382 Some(self.call::<lsp::request::CodeActionRequest>(params))
1296 }
1383 }
1297
1384
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−
1306 pub fn rename_symbol(
1385 pub fn rename_symbol(
1307 &self,
1386 &self,
1308 text_document: lsp::TextDocumentIdentifier,
1387 text_document: lsp::TextDocumentIdentifier,
1309 position: lsp::Position,
1388 position: lsp::Position,
1310 new_name: String,
1389 new_name: String,
1311 ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
1390 ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
1312− if !self.supports_rename() {
1391+ if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
1313 return None;
1392 return None;
1314 }
1393 }
1315
1394
helix-lsp/src/lib.rs+114−96
@@ -12,24 +12,21 @@ pub use lsp_types as lsp;
12use futures_util::stream::select_all::SelectAll;
12use futures_util::stream::select_all::SelectAll;
13use helix_core::{
13use helix_core::{
14 path,
14 path,
15− syntax::{LanguageConfiguration, LanguageServerConfiguration},
15+ syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
16};
16};
17use tokio::sync::mpsc::UnboundedReceiver;
17use tokio::sync::mpsc::UnboundedReceiver;
18
18
19use std::{
19use std::{
20− collections::{hash_map::Entry, HashMap},
20+ collections::HashMap,
21 path::{Path, PathBuf},
21 path::{Path, PathBuf},
22− sync::{
22+ sync::Arc,
23− atomic::{AtomicUsize, Ordering},
24− Arc,
25− },
26};
23};
27
24
28use thiserror::Error;
25use thiserror::Error;
29use tokio_stream::wrappers::UnboundedReceiverStream;
26use tokio_stream::wrappers::UnboundedReceiverStream;
30
27
31pub type Result<T> = core::result::Result<T, Error>;
28pub type Result<T> = core::result::Result<T, Error>;
32−type LanguageId = String;
29+pub type LanguageServerName = String;
33
30
34#[derive(Error, Debug)]
31#[derive(Error, Debug)]
35pub enum Error {
32pub enum Error {
@@ -49,7 +46,7 @@ pub enum Error {
49 Other(#[from] anyhow::Error),
46 Other(#[from] anyhow::Error),
50}
47}
51
48
52−#[derive(Clone, Copy, Debug, Default)]
49+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub enum OffsetEncoding {
50pub enum OffsetEncoding {
54 /// UTF-8 code units aka bytes
51 /// UTF-8 code units aka bytes
55 Utf8,
52 Utf8,
@@ -624,23 +621,18 @@ impl Notification {
624
621
625#[derive(Debug)]
622#[derive(Debug)]
626pub struct Registry {
623pub struct Registry {
627− inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
624+ inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
628−
625+ syn_loader: Arc<helix_core::syntax::Loader>,
629− counter: AtomicUsize,
626+ counter: usize,
630 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
627 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
631}
628}
632
629
633−impl Default for Registry {
634− fn default() -> Self {
635− Self::new()
636− }
637−}
638−
639impl Registry {
630impl Registry {
640− pub fn new() -> Self {
631+ pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
641 Self {
632 Self {
642 inner: HashMap::new(),
633 inner: HashMap::new(),
634+ syn_loader,
643− counter: AtomicUsize::new(0),
635+ counter: 0,
644 incoming: SelectAll::new(),
636 incoming: SelectAll::new(),
645 }
637 }
646 }
638 }
@@ -649,65 +641,92 @@ impl Registry {
649 self.inner
641 self.inner
650 .values()
642 .values()
651 .flatten()
643 .flatten()
652− .find(|(client_id, _)| client_id == &id)
644+ .find(|client| client.id() == id)
653− .map(|(_, client)| client.as_ref())
645+ .map(|client| &**client)
654 }
646 }
655
647
656 pub fn remove_by_id(&mut self, id: usize) {
648 pub fn remove_by_id(&mut self, id: usize) {
657− self.inner.retain(|_, clients| {
649+ self.inner.retain(|_, language_servers| {
658− clients.retain(|&(client_id, _)| client_id != id);
650+ language_servers.retain(|ls| id != ls.id());
659− !clients.is_empty()
651+ !language_servers.is_empty()
660− })
652+ });
661 }
653 }
662
654
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
663 pub fn restart(
686 pub fn restart(
664 &mut self,
687 &mut self,
665 language_config: &LanguageConfiguration,
688 language_config: &LanguageConfiguration,
666 doc_path: Option<&std::path::PathBuf>,
689 doc_path: Option<&std::path::PathBuf>,
667 root_dirs: &[PathBuf],
690 root_dirs: &[PathBuf],
668 enable_snippets: bool,
691 enable_snippets: bool,
669− ) -> Result<Option<Arc<Client>>> {
692+ ) -> Result<Vec<Arc<Client>>> {
670− let config = match &language_config.language_server {
693+ language_config
671− Some(config) => config,
694+ .language_servers
672− None => return Ok(None),
695+ .iter()
673− };
696+ .filter_map(|LanguageServerFeatures { name, .. }| {
674−
697+ if self.inner.contains_key(name) {
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(
698+ let client = match self.start_client(
684− id,
699+ name.clone(),
685− language_config,
700+ language_config,
686− config,
687− doc_path,
701+ doc_path,
688− root_dirs,
702+ root_dirs,
689− enable_snippets,
703+ enable_snippets,
690− )?;
704+ ) {
691− self.incoming.push(UnboundedReceiverStream::new(incoming));
705+ Ok(client) => client,
692−
706+ error => return Some(error),
707+ };
708+ let old_clients = self
709+ .inner
693− let old_clients = entry.insert(vec![(id, client.clone())]);
710+ .insert(name.clone(), vec![client.clone()])
694−
711+ .unwrap();
712+
695− for (_, old_client) in old_clients {
713+ for old_client in old_clients {
696− tokio::spawn(async move {
714+ tokio::spawn(async move {
697− let _ = old_client.force_shutdown().await;
715+ let _ = old_client.force_shutdown().await;
698− });
716+ });
717+ }
718+
719+ Some(Ok(client))
720+ } else {
721+ None
699 }
722 }
700−
723+ })
701− Ok(Some(client))
724+ .collect()
702− }
703− }
704 }
725 }
705
726
706− pub fn stop(&mut self, language_config: &LanguageConfiguration) {
727+ pub fn stop(&mut self, name: &str) {
707− let scope = language_config.scope.clone();
708−
709− if let Some(clients) = self.inner.remove(&scope) {
728+ if let Some(clients) = self.inner.remove(name) {
710− for (_, client) in clients {
729+ for client in clients {
711 tokio::spawn(async move {
730 tokio::spawn(async move {
712 let _ = client.force_shutdown().await;
731 let _ = client.force_shutdown().await;
713 });
732 });
@@ -721,37 +740,34 @@ impl Registry {
721 doc_path: Option<&std::path::PathBuf>,
740 doc_path: Option<&std::path::PathBuf>,
722 root_dirs: &[PathBuf],
741 root_dirs: &[PathBuf],
723 enable_snippets: bool,
742 enable_snippets: bool,
724− ) -> Result<Option<Arc<Client>>> {
743+ ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
725− let config = match &language_config.language_server {
744+ language_config
726− Some(config) => config,
745+ .language_servers
727− None => return Ok(None),
746+ .iter()
728− };
747+ .map(|LanguageServerFeatures { name, .. }| {
729−
730− let clients = self.inner.entry(language_config.scope.clone()).or_default();
748+ if let Some(clients) = self.inner.get(name) {
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))| {
749+ if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
733− client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
750+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
734− }) {
751+ }) {
735− return Ok(Some(client.1.clone()));
752+ return Ok((name.to_owned(), client.clone()));
736− }
753+ }
737− // initialize a new client
754+ }
738− let id = self.counter.fetch_add(1, Ordering::Relaxed);
755+ let client = self.start_client(
739−
756+ name.clone(),
740− let NewClientResult(client, incoming) = start_client(
741− id,
742− language_config,
757+ language_config,
743− config,
744− doc_path,
758+ doc_path,
745− root_dirs,
759+ root_dirs,
746− enable_snippets,
760+ enable_snippets,
747− )?;
761+ )?;
748− clients.push((id, client.clone()));
762+ let clients = self.inner.entry(name.clone()).or_default();
749− self.incoming.push(UnboundedReceiverStream::new(incoming));
763+ clients.push(client.clone());
750− Ok(Some(client))
764+ Ok((name.clone(), client))
765+ })
766+ .collect()
751 }
767 }
752
768
753 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
769 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
754− self.inner.values().flatten().map(|(_, client)| client)
770+ self.inner.values().flatten()
755 }
771 }
756}
772}
757
773
@@ -833,26 +849,28 @@ impl LspProgressMap {
833 }
849 }
834}
850}
835
851
836−struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
852+struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
837
853
838/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
854/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
839/// it is only called when it makes sense.
855/// it is only called when it makes sense.
840fn start_client(
856fn start_client(
841 id: usize,
857 id: usize,
858+ name: String,
842 config: &LanguageConfiguration,
859 config: &LanguageConfiguration,
843 ls_config: &LanguageServerConfiguration,
860 ls_config: &LanguageServerConfiguration,
844 doc_path: Option<&std::path::PathBuf>,
861 doc_path: Option<&std::path::PathBuf>,
845 root_dirs: &[PathBuf],
862 root_dirs: &[PathBuf],
846 enable_snippets: bool,
863 enable_snippets: bool,
847−) -> Result<NewClientResult> {
864+) -> Result<NewClient> {
848 let (client, incoming, initialize_notify) = Client::start(
865 let (client, incoming, initialize_notify) = Client::start(
849 &ls_config.command,
866 &ls_config.command,
850 &ls_config.args,
867 &ls_config.args,
851− config.config.clone(),
868+ ls_config.config.clone(),
852 ls_config.environment.clone(),
869 ls_config.environment.clone(),
853 &config.roots,
870 &config.roots,
854 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
871 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
855 id,
872 id,
873+ name,
856 ls_config.timeout,
874 ls_config.timeout,
857 doc_path,
875 doc_path,
858 )?;
876 )?;
@@ -886,7 +904,7 @@ fn start_client(
886 initialize_notify.notify_one();
904 initialize_notify.notify_one();
887 });
905 });
888
906
889− Ok(NewClientResult(client, incoming))
907+ Ok(NewClient(client, incoming))
890}
908}
891
909
892/// Find an LSP workspace of a file using the following mechanism:
910/// Find an LSP workspace of a file using the following mechanism:
helix-lsp/src/transport.rs+43−20
@@ -38,6 +38,7 @@ enum ServerMessage {
38#[derive(Debug)]
38#[derive(Debug)]
39pub struct Transport {
39pub struct Transport {
40 id: usize,
40 id: usize,
41+ name: String,
41 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
42 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
42}
43}
43
44
@@ -47,6 +48,7 @@ impl Transport {
47 server_stdin: BufWriter<ChildStdin>,
48 server_stdin: BufWriter<ChildStdin>,
48 server_stderr: BufReader<ChildStderr>,
49 server_stderr: BufReader<ChildStderr>,
49 id: usize,
50 id: usize,
51+ name: String,
50 ) -> (
52 ) -> (
51 UnboundedReceiver<(usize, jsonrpc::Call)>,
53 UnboundedReceiver<(usize, jsonrpc::Call)>,
52 UnboundedSender<Payload>,
54 UnboundedSender<Payload>,
@@ -58,6 +60,7 @@ impl Transport {
58
60
59 let transport = Self {
61 let transport = Self {
60 id,
62 id,
63+ name,
61 pending_requests: Mutex::new(HashMap::default()),
64 pending_requests: Mutex::new(HashMap::default()),
62 };
65 };
63
66
@@ -83,6 +86,7 @@ impl Transport {
83 async fn recv_server_message(
86 async fn recv_server_message(
84 reader: &mut (impl AsyncBufRead + Unpin + Send),
87 reader: &mut (impl AsyncBufRead + Unpin + Send),
85 buffer: &mut String,
88 buffer: &mut String,
89+ language_server_name: &str,
86 ) -> Result<ServerMessage> {
90 ) -> Result<ServerMessage> {
87 let mut content_length = None;
91 let mut content_length = None;
88 loop {
92 loop {
@@ -124,7 +128,7 @@ impl Transport {
124 reader.read_exact(&mut content).await?;
128 reader.read_exact(&mut content).await?;
125 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
129 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
126
130
127− info!("<- {}", msg);
131+ info!("{language_server_name} <- {msg}");
128
132
129 // try parsing as output (server response) or call (server request)
133 // try parsing as output (server response) or call (server request)
130 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
134 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@@ -135,12 +139,13 @@ impl Transport {
135 async fn recv_server_error(
139 async fn recv_server_error(
136 err: &mut (impl AsyncBufRead + Unpin + Send),
140 err: &mut (impl AsyncBufRead + Unpin + Send),
137 buffer: &mut String,
141 buffer: &mut String,
142+ language_server_name: &str,
138 ) -> Result<()> {
143 ) -> Result<()> {
139 buffer.truncate(0);
144 buffer.truncate(0);
140 if err.read_line(buffer).await? == 0 {
145 if err.read_line(buffer).await? == 0 {
141 return Err(Error::StreamClosed);
146 return Err(Error::StreamClosed);
142 };
147 };
143− error!("err <- {:?}", buffer);
148+ error!("{language_server_name} err <- {buffer:?}");
144
149
145 Ok(())
150 Ok(())
146 }
151 }
@@ -162,15 +167,17 @@ impl Transport {
162 Payload::Notification(value) => serde_json::to_string(&value)?,
167 Payload::Notification(value) => serde_json::to_string(&value)?,
163 Payload::Response(error) => serde_json::to_string(&error)?,
168 Payload::Response(error) => serde_json::to_string(&error)?,
164 };
169 };
165− self.send_string_to_server(server_stdin, json).await
170+ self.send_string_to_server(server_stdin, json, &self.name)
171+ .await
166 }
172 }
167
173
168 async fn send_string_to_server(
174 async fn send_string_to_server(
169 &self,
175 &self,
170 server_stdin: &mut BufWriter<ChildStdin>,
176 server_stdin: &mut BufWriter<ChildStdin>,
171 request: String,
177 request: String,
178+ language_server_name: &str,
172 ) -> Result<()> {
179 ) -> Result<()> {
173− info!("-> {}", request);
180+ info!("{language_server_name} -> {request}");
174
181
175 // send the headers
182 // send the headers
176 server_stdin
183 server_stdin
@@ -189,9 +196,13 @@ impl Transport {
189 &self,
196 &self,
190 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
197 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
191 msg: ServerMessage,
198 msg: ServerMessage,
199+ language_server_name: &str,
192 ) -> Result<()> {
200 ) -> Result<()> {
193 match msg {
201 match msg {
202+ ServerMessage::Output(output) => {
194− ServerMessage::Output(output) => self.process_request_response(output).await?,
203+ self.process_request_response(output, language_server_name)
204+ .await?
205+ }
195 ServerMessage::Call(call) => {
206 ServerMessage::Call(call) => {
196 client_tx
207 client_tx
197 .send((self.id, call))
208 .send((self.id, call))
@@ -202,14 +213,18 @@ impl Transport {
202 Ok(())
213 Ok(())
203 }
214 }
204
215
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<()> {
206 let (id, result) = match output {
221 let (id, result) = match output {
207 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
222 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
208− info!("<- {}", result);
223+ info!("{language_server_name} <- {}", result);
209 (id, Ok(result))
224 (id, Ok(result))
210 }
225 }
211 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
226 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
212− error!("<- {}", error);
227+ error!("{language_server_name} <- {error}");
213 (id, Err(error.into()))
228 (id, Err(error.into()))
214 }
229 }
215 };
230 };
@@ -240,12 +255,17 @@ impl Transport {
240 ) {
255 ) {
241 let mut recv_buffer = String::new();
256 let mut recv_buffer = String::new();
242 loop {
257 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+ {
244 Ok(msg) => {
261 Ok(msg) => {
262+ match transport
245− match transport.process_server_message(&client_tx, msg).await {
263+ .process_server_message(&client_tx, msg, &transport.name)
264+ .await
265+ {
246 Ok(_) => {}
266 Ok(_) => {}
247 Err(err) => {
267 Err(err) => {
248− error!("err: <- {:?}", err);
268+ error!("{} err: <- {err:?}", transport.name);
249 break;
269 break;
250 }
270 }
251 };
271 };
@@ -270,7 +290,7 @@ impl Transport {
270 params: jsonrpc::Params::None,
290 params: jsonrpc::Params::None,
271 }));
291 }));
272 match transport
292 match transport
273− .process_server_message(&client_tx, notification)
293+ .process_server_message(&client_tx, notification, &transport.name)
274 .await
294 .await
275 {
295 {
276 Ok(_) => {}
296 Ok(_) => {}
@@ -281,20 +301,22 @@ impl Transport {
281 break;
301 break;
282 }
302 }
283 Err(err) => {
303 Err(err) => {
284− error!("err: <- {:?}", err);
304+ error!("{} err: <- {err:?}", transport.name);
285 break;
305 break;
286 }
306 }
287 }
307 }
288 }
308 }
289 }
309 }
290
310
291− async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
311+ async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
292 let mut recv_buffer = String::new();
312 let mut recv_buffer = String::new();
293 loop {
313 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+ {
295 Ok(_) => {}
317 Ok(_) => {}
296 Err(err) => {
318 Err(err) => {
297− error!("err: <- {:?}", err);
319+ error!("{} err: <- {err:?}", transport.name);
298 break;
320 break;
299 }
321 }
300 }
322 }
@@ -348,10 +370,11 @@ impl Transport {
348 method: lsp_types::notification::Initialized::METHOD.to_string(),
370 method: lsp_types::notification::Initialized::METHOD.to_string(),
349 params: jsonrpc::Params::None,
371 params: jsonrpc::Params::None,
350 }));
372 }));
373+ let language_server_name = &transport.name;
351− match transport.process_server_message(&client_tx, notification).await {
374+ match transport.process_server_message(&client_tx, notification, language_server_name).await {
352 Ok(_) => {}
375 Ok(_) => {}
353 Err(err) => {
376 Err(err) => {
354− error!("err: <- {:?}", err);
377+ error!("{language_server_name} err: <- {err:?}");
355 }
378 }
356 }
379 }
357
380
@@ -361,7 +384,7 @@ impl Transport {
361 match transport.send_payload_to_server(&mut server_stdin, msg).await {
384 match transport.send_payload_to_server(&mut server_stdin, msg).await {
362 Ok(_) => {}
385 Ok(_) => {}
363 Err(err) => {
386 Err(err) => {
364− error!("err: <- {:?}", err);
387+ error!("{language_server_name} err: <- {err:?}");
365 }
388 }
366 }
389 }
367 }
390 }
@@ -380,7 +403,7 @@ impl Transport {
380 match transport.send_payload_to_server(&mut server_stdin, msg).await {
403 match transport.send_payload_to_server(&mut server_stdin, msg).await {
381 Ok(_) => {}
404 Ok(_) => {}
382 Err(err) => {
405 Err(err) => {
383− error!("err: <- {:?}", err);
406+ error!("{} err: <- {err:?}", transport.name);
384 }
407 }
385 }
408 }
386 }
409 }
helix-term/src/application.rs+72−73
@@ -30,6 +30,7 @@ use crate::{
30
30
31use log::{debug, error, warn};
31use log::{debug, error, warn};
32use std::{
32use std::{
33+ collections::btree_map::Entry,
33 io::{stdin, stdout},
34 io::{stdin, stdout},
34 path::Path,
35 path::Path,
35 sync::Arc,
36 sync::Arc,
@@ -564,7 +565,7 @@ impl Application {
564 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565 let id = doc.id();
566 let id = doc.id();
566 doc.detect_language(loader);
567 doc.detect_language(loader);
567− let _ = self.editor.refresh_language_server(id);
568+ self.editor.refresh_language_servers(id);
568 }
569 }
569
570
570 // TODO: fix being overwritten by lsp
571 // TODO: fix being overwritten by lsp
@@ -662,6 +663,18 @@ impl Application {
662 ) {
663 ) {
663 use helix_lsp::{Call, MethodCall, Notification};
664 use helix_lsp::{Call, MethodCall, Notification};
664
665
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+
665 match call {
678 match call {
666 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
679 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
667 let notification = match Notification::parse(&method, params) {
680 let notification = match Notification::parse(&method, params) {
@@ -677,14 +690,7 @@ impl Application {
677
690
678 match notification {
691 match notification {
679 Notification::Initialized => {
692 Notification::Initialized => {
680− let language_server =
693+ let language_server = 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− };
688
694
689 // Trigger a workspace/didChangeConfiguration notification after initialization.
695 // Trigger a workspace/didChangeConfiguration notification after initialization.
690 // This might not be required by the spec but Neovim does this as well, so it's
696 // This might not be required by the spec but Neovim does this as well, so it's
@@ -693,9 +699,10 @@ impl Application {
693 tokio::spawn(language_server.did_change_configuration(config.clone()));
699 tokio::spawn(language_server.did_change_configuration(config.clone()));
694 }
700 }
695
701
696− let docs = self.editor.documents().filter(|doc| {
702+ let docs = self
697− doc.language_server().map(|server| server.id()) == Some(server_id)
703+ .editor
698− });
704+ .documents()
705+ .filter(|doc| doc.supports_language_server(server_id));
699
706
700 // trigger textDocument/didOpen for docs that are already open
707 // trigger textDocument/didOpen for docs that are already open
701 for doc in docs {
708 for doc in docs {
@@ -715,7 +722,7 @@ impl Application {
715 ));
722 ));
716 }
723 }
717 }
724 }
718− Notification::PublishDiagnostics(mut params) => {
725+ Notification::PublishDiagnostics(params) => {
719 let path = match params.uri.to_file_path() {
726 let path = match params.uri.to_file_path() {
720 Ok(path) => path,
727 Ok(path) => path,
721 Err(_) => {
728 Err(_) => {
@@ -723,6 +730,7 @@ impl Application {
723 return;
730 return;
724 }
731 }
725 };
732 };
733+ let offset_encoding = language_server!().offset_encoding();
726 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
734 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
727 if let Some(version) = params.version {
735 if let Some(version) = params.version {
728 if version != doc.version() {
736 if version != doc.version() {
@@ -745,18 +753,11 @@ impl Application {
745 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
753 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
746 use lsp::DiagnosticSeverity;
754 use lsp::DiagnosticSeverity;
747
755
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−
755 // TODO: convert inside server
756 // TODO: convert inside server
756 let start = if let Some(start) = lsp_pos_to_pos(
757 let start = if let Some(start) = lsp_pos_to_pos(
757 text,
758 text,
758 diagnostic.range.start,
759 diagnostic.range.start,
759− language_server.offset_encoding(),
760+ offset_encoding,
760 ) {
761 ) {
761 start
762 start
762 } else {
763 } else {
@@ -764,11 +765,9 @@ impl Application {
764 return None;
765 return None;
765 };
766 };
766
767
767− let end = if let Some(end) = lsp_pos_to_pos(
768+ let end = if let Some(end) =
768− text,
769− diagnostic.range.end,
769+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
770− language_server.offset_encoding(),
771− ) {
770+ {
772 end
771 end
773 } else {
772 } else {
774 log::warn!("lsp position out of bounds - {:?}", diagnostic);
773 log::warn!("lsp position out of bounds - {:?}", diagnostic);
@@ -807,14 +806,19 @@ impl Application {
807 None => None,
806 None => None,
808 };
807 };
809
808
810− let tags = if let Some(ref tags) = diagnostic.tags {
809+ let tags = if let Some(tags) = &diagnostic.tags {
811− let new_tags = tags.iter().filter_map(|tag| {
810+ let new_tags = tags
811+ .iter()
812− match *tag {
812+ .filter_map(|tag| match *tag {
813+ lsp::DiagnosticTag::DEPRECATED => {
813− lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
814+ Some(DiagnosticTag::Deprecated)
815+ }
816+ lsp::DiagnosticTag::UNNECESSARY => {
814− lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
817+ Some(DiagnosticTag::Unnecessary)
818+ }
815− _ => None
819+ _ => None,
816− }
820+ })
817− }).collect();
821+ .collect();
818
822
819 new_tags
823 new_tags
820 } else {
824 } else {
@@ -830,25 +834,40 @@ impl Application {
830 tags,
834 tags,
831 source: diagnostic.source.clone(),
835 source: diagnostic.source.clone(),
832 data: diagnostic.data.clone(),
836 data: diagnostic.data.clone(),
837+ language_server_id: server_id,
833 })
838 })
834 })
839 })
835 .collect();
840 .collect();
836
841
837− doc.set_diagnostics(diagnostics);
842+ doc.replace_diagnostics(diagnostics, server_id);
838 }
843 }
839
844
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
843 .diagnostics
846 .diagnostics
844− .sort_unstable_by_key(|d| (d.severity, d.range.start));
847+ .into_iter()
848+ .map(|d| (d, server_id))
849+ .collect();
845
850
846 // Insert the original lsp::Diagnostics here because we may have no open document
851 // Insert the original lsp::Diagnostics here because we may have no open document
847 // for diagnosic message and so we can't calculate the exact position.
852 // for diagnosic message and so we can't calculate the exact position.
848 // When using them later in the diagnostics picker, we calculate them on-demand.
853 // When using them later in the diagnostics picker, we calculate them on-demand.
849− self.editor
850− .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));
851− .insert(params.uri, params.diagnostics);
868+ v.insert(diagnostics);
869+ }
870+ };
852 }
871 }
853 Notification::ShowMessage(params) => {
872 Notification::ShowMessage(params) => {
854 log::warn!("unhandled window/showMessage: {:?}", params);
873 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -950,10 +969,8 @@ impl Application {
950 .editor
969 .editor
951 .documents_mut()
970 .documents_mut()
952 .filter_map(|doc| {
971 .filter_map(|doc| {
953− if doc.language_server().map(|server| server.id())
972+ if doc.supports_language_server(server_id) {
954− == Some(server_id)
973+ doc.clear_diagnostics(server_id);
955− {
956− doc.set_diagnostics(Vec::new());
957 doc.url()
974 doc.url()
958 } else {
975 } else {
959 None
976 None
@@ -1029,31 +1046,21 @@ impl Application {
1029 }))
1046 }))
1030 }
1047 }
1031 Ok(MethodCall::WorkspaceFolders) => {
1048 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))
1036 }
1050 }
1037 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1051 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1052+ let language_server = language_server!();
1038 let result: Vec<_> = params
1053 let result: Vec<_> = params
1039 .items
1054 .items
1040 .iter()
1055 .iter()
1041 .map(|item| {
1056 .map(|item| {
1042− let mut config = match &item.scope_uri {
1057+ let mut config = language_server.config()?;
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− };
1054 if let Some(section) = item.section.as_ref() {
1058 if let Some(section) = item.section.as_ref() {
1059+ // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
1060+ if !section.is_empty() {
1055− for part in section.split('.') {
1061+ for part in section.split('.') {
1056− config = config.get(part)?;
1062+ config = config.get(part)?;
1063+ }
1057 }
1064 }
1058 }
1065 }
1059 Some(config)
1066 Some(config)
@@ -1074,15 +1081,7 @@ impl Application {
1074 }
1081 }
1075 };
1082 };
1076
1083
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));
1086 }
1085 }
1087 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
1086 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
1088 }
1087 }
helix-term/src/commands.rs+117−94
@@ -23,6 +23,7 @@ use helix_core::{
23 regex::{self, Regex, RegexBuilder},
23 regex::{self, Regex, RegexBuilder},
24 search::{self, CharMatcher},
24 search::{self, CharMatcher},
25 selection, shellwords, surround,
25 selection, shellwords, surround,
26+ syntax::LanguageServerFeature,
26 text_annotations::TextAnnotations,
27 text_annotations::TextAnnotations,
27 textobject,
28 textobject,
28 tree_sitter::Node,
29 tree_sitter::Node,
@@ -54,13 +55,13 @@ use crate::{
54 job::Callback,
55 job::Callback,
55 keymap::ReverseKeymap,
56 keymap::ReverseKeymap,
56 ui::{
57 ui::{
57− self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
58+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
58− Popup, Prompt, PromptEvent,
59+ FilePicker, Picker, Popup, Prompt, PromptEvent,
59 },
60 },
60};
61};
61
62
62use crate::job::{self, Jobs};
63use crate::job::{self, Jobs};
63−use futures_util::StreamExt;
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
64use std::{collections::HashMap, fmt, future::Future};
65use std::{collections::HashMap, fmt, future::Future};
65use std::{collections::HashSet, num::NonZeroUsize};
66use std::{collections::HashSet, num::NonZeroUsize};
66
67
@@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
3029
3030
3030fn goto_first_diag(cx: &mut Context) {
3031fn goto_first_diag(cx: &mut Context) {
3031 let (view, doc) = current!(cx.editor);
3032 let (view, doc) = current!(cx.editor);
3032− let selection = match doc.diagnostics().first() {
3033+ let selection = match doc.shown_diagnostics().next() {
3033 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3034 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3034 None => return,
3035 None => return,
3035 };
3036 };
@@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
3038
3039
3039fn goto_last_diag(cx: &mut Context) {
3040fn goto_last_diag(cx: &mut Context) {
3040 let (view, doc) = current!(cx.editor);
3041 let (view, doc) = current!(cx.editor);
3041− let selection = match doc.diagnostics().last() {
3042+ let selection = match doc.shown_diagnostics().last() {
3042 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3043 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3043 None => return,
3044 None => return,
3044 };
3045 };
@@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
3054 .cursor(doc.text().slice(..));
3055 .cursor(doc.text().slice(..));
3055
3056
3056 let diag = doc
3057 let diag = doc
3057− .diagnostics()
3058+ .shown_diagnostics()
3058− .iter()
3059 .find(|diag| diag.range.start > cursor_pos)
3059 .find(|diag| diag.range.start > cursor_pos)
3060− .or_else(|| doc.diagnostics().first());
3060+ .or_else(|| doc.shown_diagnostics().next());
3061
3061
3062 let selection = match diag {
3062 let selection = match diag {
3063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
@@ -3075,11 +3075,10 @@ fn goto_prev_diag(cx: &mut Context) {
3075 .cursor(doc.text().slice(..));
3075 .cursor(doc.text().slice(..));
3076
3076
3077 let diag = doc
3077 let diag = doc
3078− .diagnostics()
3078+ .shown_diagnostics()
3079− .iter()
3080 .rev()
3079 .rev()
3081 .find(|diag| diag.range.start < cursor_pos)
3080 .find(|diag| diag.range.start < cursor_pos)
3082− .or_else(|| doc.diagnostics().last());
3081+ .or_else(|| doc.shown_diagnostics().last());
3083
3082
3084 let selection = match diag {
3083 let selection = match diag {
3085 // NOTE: the selection is reversed because we're jumping to the
3084 // NOTE: the selection is reversed because we're jumping to the
@@ -3234,23 +3233,19 @@ pub mod insert {
3234 use helix_lsp::lsp;
3233 use helix_lsp::lsp;
3235 // if ch matches completion char, trigger completion
3234 // if ch matches completion char, trigger completion
3236 let doc = doc_mut!(cx.editor);
3235 let doc = doc_mut!(cx.editor);
3237− let language_server = match doc.language_server() {
3236+ let trigger_completion = doc
3238− Some(language_server) => language_server,
3237+ .language_servers_with_feature(LanguageServerFeature::Completion)
3239− None => return,
3238+ .any(|ls| {
3240− };
3239+ // TODO: what if trigger is multiple chars long
3241−
3240+ matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
3242− let capabilities = language_server.capabilities();
3241+ trigger_characters: Some(triggers),
3242+ ..
3243+ }) if triggers.iter().any(|trigger| trigger.contains(ch)))
3244+ });
3243
3245
3244− if let Some(lsp::CompletionOptions {
3246+ if trigger_completion {
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();
3247+ cx.editor.clear_idle_timer();
3252− super::completion(cx);
3248+ super::completion(cx);
3253− }
3254 }
3249 }
3255 }
3250 }
3256
3251
@@ -3258,12 +3253,12 @@ pub mod insert {
3258 use helix_lsp::lsp;
3253 use helix_lsp::lsp;
3259 // if ch matches signature_help char, trigger
3254 // if ch matches signature_help char, trigger
3260 let doc = doc_mut!(cx.editor);
3255 let doc = doc_mut!(cx.editor);
3261− // The language_server!() macro is not used here since it will
3256+ // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
3262− // print an "LSP not active for current buffer" message on
3263− // every keypress.
3264− let language_server = match doc.language_server() {
3257+ let Some(language_server) = doc
3265− Some(language_server) => language_server,
3258+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
3259+ .next()
3260+ else {
3266− None => return,
3261+ return;
3267 };
3262 };
3268
3263
3269 let capabilities = language_server.capabilities();
3264 let capabilities = language_server.capabilities();
@@ -4046,55 +4041,60 @@ fn format_selections(cx: &mut Context) {
4046 use helix_lsp::{lsp, util::range_to_lsp_range};
4041 use helix_lsp::{lsp, util::range_to_lsp_range};
4047
4042
4048 let (view, doc) = current!(cx.editor);
4043 let (view, doc) = current!(cx.editor);
4044+ let view_id = view.id;
4049
4045
4050 // via lsp if available
4046 // via lsp if available
4051 // TODO: else via tree-sitter indentation calculations
4047 // TODO: else via tree-sitter indentation calculations
4052
4048
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
4053− let language_server = match doc.language_server() {
4057+ let Some(language_server) = doc
4054− Some(language_server) => language_server,
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");
4055− None => return,
4068+ return;
4056 };
4069 };
4057
4070
4071+ let offset_encoding = language_server.offset_encoding();
4058 let ranges: Vec<lsp::Range> = doc
4072 let ranges: Vec<lsp::Range> = doc
4059− .selection(view.id)
4073+ .selection(view_id)
4060 .iter()
4074 .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))
4062 .collect();
4076 .collect();
4063
4077
4064− if ranges.len() != 1 {
4065− cx.editor
4066− .set_error("format_selections only supports a single selection for now");
4067− return;
4068− }
4069−
4070 // TODO: handle fails
4078 // TODO: handle fails
4071 // TODO: concurrent map over all ranges
4079 // TODO: concurrent map over all ranges
4072
4080
4073 let range = ranges[0];
4081 let range = ranges[0];
4074
4082
4083+ let future = language_server
4075− let request = match language_server.text_document_range_formatting(
4084+ .text_document_range_formatting(
4076− doc.identifier(),
4085+ doc.identifier(),
4077− range,
4086+ range,
4078− lsp::FormattingOptions::default(),
4087+ lsp::FormattingOptions::default(),
4079− None,
4088+ None,
4080− ) {
4089+ )
4081− Some(future) => future,
4090+ .unwrap();
4082− None => {
4083− cx.editor
4084− .set_error("Language server does not support range formatting");
4085− return;
4086− }
4087− };
4088
4091
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();
4090
4093
4094+ let transaction =
4091− let transaction = helix_lsp::util::generate_transaction_from_edits(
4095+ helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
4092− doc.text(),
4093− edits,
4094− language_server.offset_encoding(),
4095− );
4096
4096
4097− doc.apply(&transaction, view.id);
4097+ doc.apply(&transaction, view_id);
4098}
4098}
4099
4099
4100fn join_selections_impl(cx: &mut Context, select_space: bool) {
4100fn join_selections_impl(cx: &mut Context, select_space: bool) {
@@ -4231,21 +4231,46 @@ pub fn completion(cx: &mut Context) {
4231 doc.savepoint(view)
4231 doc.savepoint(view)
4232 };
4232 };
4233
4233
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();
4240 let text = savepoint.text.clone();
4234 let text = savepoint.text.clone();
4241 let cursor = savepoint.cursor();
4235 let cursor = savepoint.cursor();
4242
4236
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();
4243− let pos = pos_to_lsp_pos(&text, cursor, 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();
4244
4269
4245− let future = match language_server.completion(doc.identifier(), pos, None) {
4270+ anyhow::Ok(items)
4246− Some(future) => future,
4271+ }
4247− None => return,
4272+ })
4248− };
4273+ .collect();
4249
4274
4250 // setup a channel that allows the request to be canceled
4275 // setup a channel that allows the request to be canceled
4251 let (tx, rx) = oneshot::channel();
4276 let (tx, rx) = oneshot::channel();
@@ -4254,12 +4279,20 @@ pub fn completion(cx: &mut Context) {
4254 // and the associated request is automatically dropped
4279 // and the associated request is automatically dropped
4255 cx.editor.completion_request_handle = Some(tx);
4280 cx.editor.completion_request_handle = Some(tx);
4256 let future = async move {
4281 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+ };
4257 tokio::select! {
4290 tokio::select! {
4258 biased;
4291 biased;
4259 _ = rx => {
4292 _ = rx => {
4260− Ok(serde_json::Value::Null)
4293+ Ok(Vec::new())
4261 }
4294 }
4262− res = future => {
4295+ res = items_future => {
4263 res
4296 res
4264 }
4297 }
4265 }
4298 }
@@ -4293,9 +4326,9 @@ pub fn completion(cx: &mut Context) {
4293 },
4326 },
4294 ));
4327 ));
4295
4328
4296− cx.callback(
4329+ cx.jobs.callback(async move {
4297− future,
4330+ let items = future.await?;
4298− move |editor, compositor, response: Option<lsp::CompletionResponse>| {
4331+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
4299 let (view, doc) = current_ref!(editor);
4332 let (view, doc) = current_ref!(editor);
4300 // check if the completion request is stale.
4333 // check if the completion request is stale.
4301 //
4334 //
@@ -4306,16 +4339,6 @@ pub fn completion(cx: &mut Context) {
4306 return;
4339 return;
4307 }
4340 }
4308
4341
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−
4319 if items.is_empty() {
4342 if items.is_empty() {
4320 // editor.set_error("No completion available");
4343 // editor.set_error("No completion available");
4321 return;
4344 return;
@@ -4326,7 +4349,6 @@ pub fn completion(cx: &mut Context) {
4326 editor,
4349 editor,
4327 savepoint,
4350 savepoint,
4328 items,
4351 items,
4329− offset_encoding,
4330 start_offset,
4352 start_offset,
4331 trigger_offset,
4353 trigger_offset,
4332 size,
4354 size,
@@ -4340,8 +4362,9 @@ pub fn completion(cx: &mut Context) {
4340 {
4362 {
4341 compositor.remove(SignatureHelp::ID);
4363 compositor.remove(SignatureHelp::ID);
4342 }
4364 }
4343− },
4365+ };
4366+ Ok(Callback::EditorCompositor(Box::new(call)))
4344− );
4367+ });
4345}
4368}
4346
4369
4347// comments
4370// comments
@@ -5141,7 +5164,7 @@ async fn shell_impl_async(
5141 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
5164 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
5142 .await?;
5165 .await?;
5143 }
5166 }
5144− Ok::<_, anyhow::Error>(())
5167+ anyhow::Ok(())
5145 });
5168 });
5146 let (output, _) = tokio::join! {
5169 let (output, _) = tokio::join! {
5147 process.wait_with_output(),
5170 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};
2use helix_lsp::{
2use helix_lsp::{
3 block_on,
3 block_on,
4 lsp::{
4 lsp::{
@@ -6,8 +6,10 @@ use helix_lsp::{
6 NumberOrString,
6 NumberOrString,
7 },
7 },
8 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
8 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
9− OffsetEncoding,
9+ Client, OffsetEncoding,
10};
10};
11+use serde_json::Value;
12+use tokio_stream::StreamExt;
11use tui::{
13use tui::{
12 text::{Span, Spans},
14 text::{Span, Spans},
13 widgets::Row,
15 widgets::Row,
@@ -15,7 +17,9 @@ use tui::{
15
17
16use super::{align_view, push_jump, Align, Context, Editor, Open};
18use super::{align_view, push_jump, Align, Context, Editor, Open};
17
19
20+use helix_core::{
18−use helix_core::{path, text_annotations::InlineAnnotation, Selection};
21+ path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
22+};
19use helix_view::{
23use helix_view::{
20 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
21 editor::Action,
25 editor::Action,
@@ -25,6 +29,7 @@ use helix_view::{
25
29
26use crate::{
30use crate::{
27 compositor::{self, Compositor},
31 compositor::{self, Compositor},
32+ job::Callback,
28 ui::{
33 ui::{
29 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
34 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
30 Popup, PromptEvent,
35 Popup, PromptEvent,
@@ -32,25 +37,34 @@ use crate::{
32};
37};
33
38
34use std::{
39use std::{
40+ cmp::Ordering,
35− cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
41+ collections::{BTreeMap, HashSet},
42+ fmt::Write,
43+ future::Future,
44+ path::PathBuf,
45+ sync::Arc,
36};
46};
37
47
38−/// Gets the language server that is attached to a document, and
48+/// Gets the first language server that is attached to a document which supports a specific feature.
39−/// if it's not active displays a status message. Using this macro
49+/// If there is no configured language server that supports the feature, this displays a status message.
40−/// in a context where the editor automatically queries the LSP
50+/// Using this macro in a context where the editor automatically queries the LSP
41−/// (instead of when the user explicitly does so via a keybind like
51+/// (instead of when the user explicitly does so via a keybind like `gd`)
42−/// `gd`) will spam the "LSP inactive" status message confusingly.
52+/// will spam the "No configured language server supports <feature>" status message confusingly.
43#[macro_export]
53#[macro_export]
44−macro_rules! language_server {
54+macro_rules! language_server_with_feature {
45− ($editor:expr, $doc:expr) => {
55+ ($editor:expr, $doc:expr, $feature:expr) => {{
56+ let language_server = $doc.language_servers_with_feature($feature).next();
46− match $doc.language_server() {
57+ match language_server {
47 Some(language_server) => language_server,
58 Some(language_server) => language_server,
48 None => {
59 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+ ));
50 return;
64 return;
51 }
65 }
52 }
66 }
53− };
67+ }};
54}
68}
55
69
56impl ui::menu::Item for lsp::Location {
70impl ui::menu::Item for lsp::Location {
@@ -87,20 +101,30 @@ impl ui::menu::Item for lsp::Location {
87 }
101 }
88}
102}
89
103
104+struct SymbolInformationItem {
105+ symbol: lsp::SymbolInformation,
106+ offset_encoding: OffsetEncoding,
107+}
108+
90−impl ui::menu::Item for lsp::SymbolInformation {
109+impl ui::menu::Item for SymbolInformationItem {
91 /// Path to currently focussed document
110 /// Path to currently focussed document
92 type Data = Option<lsp::Url>;
111 type Data = Option<lsp::Url>;
93
112
94 fn format(&self, current_doc_path: &Self::Data) -> Row {
113 fn format(&self, current_doc_path: &Self::Data) -> Row {
95− if current_doc_path.as_ref() == Some(&self.location.uri) {
114+ if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
96− self.name.as_str().into()
115+ self.symbol.name.as_str().into()
97 } else {
116 } else {
98− match self.location.uri.to_file_path() {
117+ match self.symbol.location.uri.to_file_path() {
99 Ok(path) => {
118 Ok(path) => {
100 let get_relative_path = path::get_relative_path(path.as_path());
119 let get_relative_path = path::get_relative_path(path.as_path());
120+ format!(
121+ "{} ({})",
122+ &self.symbol.name,
101− format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
123+ get_relative_path.to_string_lossy()
124+ )
125+ .into()
102 }
126 }
103− Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
127+ Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
104 }
128 }
105 }
129 }
106 }
130 }
@@ -116,6 +140,7 @@ struct DiagnosticStyles {
116struct PickerDiagnostic {
140struct PickerDiagnostic {
117 url: lsp::Url,
141 url: lsp::Url,
118 diag: lsp::Diagnostic,
142 diag: lsp::Diagnostic,
143+ offset_encoding: OffsetEncoding,
119}
144}
120
145
121impl ui::menu::Item for PickerDiagnostic {
146impl ui::menu::Item for PickerDiagnostic {
@@ -211,21 +236,19 @@ fn jump_to_location(
211 align_view(doc, view, Align::Center);
236 align_view(doc, view, Align::Center);
212}
237}
213
238
214−fn sym_picker(
239+type SymbolPicker = FilePicker<SymbolInformationItem>;
215− symbols: Vec<lsp::SymbolInformation>,
240+
216− current_path: Option<lsp::Url>,
241+fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
217− offset_encoding: OffsetEncoding,
218−) -> FilePicker<lsp::SymbolInformation> {
219 // TODO: drop current_path comparison and instead use workspace: bool flag?
242 // TODO: drop current_path comparison and instead use workspace: bool flag?
220 FilePicker::new(
243 FilePicker::new(
221 symbols,
244 symbols,
222 current_path.clone(),
245 current_path.clone(),
223− move |cx, symbol, action| {
246+ move |cx, item, action| {
224 let (view, doc) = current!(cx.editor);
247 let (view, doc) = current!(cx.editor);
225 push_jump(view, doc);
248 push_jump(view, doc);
226
249
227− if current_path.as_ref() != Some(&symbol.location.uri) {
250+ if current_path.as_ref() != Some(&item.symbol.location.uri) {
228− let uri = &symbol.location.uri;
251+ let uri = &item.symbol.location.uri;
229 let path = match uri.to_file_path() {
252 let path = match uri.to_file_path() {
230 Ok(path) => path,
253 Ok(path) => path,
231 Err(_) => {
254 Err(_) => {
@@ -245,7 +268,7 @@ fn sym_picker(
245 let (view, doc) = current!(cx.editor);
268 let (view, doc) = current!(cx.editor);
246
269
247 if let Some(range) =
270 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)
249 {
272 {
250 // we flip the range so that the cursor sits on the start of the symbol
273 // we flip the range so that the cursor sits on the start of the symbol
251 // (for example start of the function).
274 // (for example start of the function).
@@ -253,7 +276,7 @@ fn sym_picker(
253 align_view(doc, view, Align::Center);
276 align_view(doc, view, Align::Center);
254 }
277 }
255 },
278 },
256− move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
279+ move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
257 )
280 )
258 .truncate_start(false)
281 .truncate_start(false)
259}
282}
@@ -266,10 +289,9 @@ enum DiagnosticsFormat {
266
289
267fn diag_picker(
290fn diag_picker(
268 cx: &Context,
291 cx: &Context,
269− diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
292+ diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
270 current_path: Option<lsp::Url>,
293 current_path: Option<lsp::Url>,
271 format: DiagnosticsFormat,
294 format: DiagnosticsFormat,
272− offset_encoding: OffsetEncoding,
273) -> FilePicker<PickerDiagnostic> {
295) -> FilePicker<PickerDiagnostic> {
274 // TODO: drop current_path comparison and instead use workspace: bool flag?
296 // TODO: drop current_path comparison and instead use workspace: bool flag?
275
297
@@ -277,11 +299,15 @@ fn diag_picker(
277 let mut flat_diag = Vec::new();
299 let mut flat_diag = Vec::new();
278 for (url, diags) in diagnostics {
300 for (url, diags) in diagnostics {
279 flat_diag.reserve(diags.len());
301 flat_diag.reserve(diags.len());
302+
280− for diag in diags {
303+ for (diag, ls) in diags {
304+ if let Some(ls) = cx.editor.language_server_by_id(ls) {
281− flat_diag.push(PickerDiagnostic {
305+ flat_diag.push(PickerDiagnostic {
282− url: url.clone(),
306+ url: url.clone(),
283− diag,
307+ diag,
308+ offset_encoding: ls.offset_encoding(),
284− });
309+ });
310+ }
285 }
311 }
286 }
312 }
287
313
@@ -295,7 +321,13 @@ fn diag_picker(
295 FilePicker::new(
321 FilePicker::new(
296 flat_diag,
322 flat_diag,
297 (styles, format),
323 (styles, format),
324+ move |cx,
298− move |cx, PickerDiagnostic { url, diag }, action| {
325+ PickerDiagnostic {
326+ url,
327+ diag,
328+ offset_encoding,
329+ },
330+ action| {
299 if current_path.as_ref() == Some(url) {
331 if current_path.as_ref() == Some(url) {
300 let (view, doc) = current!(cx.editor);
332 let (view, doc) = current!(cx.editor);
301 push_jump(view, doc);
333 push_jump(view, doc);
@@ -306,14 +338,14 @@ fn diag_picker(
306
338
307 let (view, doc) = current!(cx.editor);
339 let (view, doc) = current!(cx.editor);
308
340
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) {
310 // we flip the range so that the cursor sits on the start of the symbol
342 // we flip the range so that the cursor sits on the start of the symbol
311 // (for example start of the function).
343 // (for example start of the function).
312 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
344 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
313 align_view(doc, view, Align::Center);
345 align_view(doc, view, Align::Center);
314 }
346 }
315 },
347 },
316− move |_editor, PickerDiagnostic { url, diag }| {
348+ move |_editor, PickerDiagnostic { url, diag, .. }| {
317 let location = lsp::Location::new(url.clone(), diag.range);
349 let location = lsp::Location::new(url.clone(), diag.range);
318 Some(location_to_file_location(&location))
350 Some(location_to_file_location(&location))
319 },
351 },
@@ -323,126 +355,154 @@ fn diag_picker(
323
355
324pub fn symbol_picker(cx: &mut Context) {
356pub fn symbol_picker(cx: &mut Context) {
325 fn nested_to_flat(
357 fn nested_to_flat(
326− list: &mut Vec<lsp::SymbolInformation>,
358+ list: &mut Vec<SymbolInformationItem>,
327 file: &lsp::TextDocumentIdentifier,
359 file: &lsp::TextDocumentIdentifier,
328 symbol: lsp::DocumentSymbol,
360 symbol: lsp::DocumentSymbol,
361+ offset_encoding: OffsetEncoding,
329 ) {
362 ) {
330 #[allow(deprecated)]
363 #[allow(deprecated)]
364+ list.push(SymbolInformationItem {
331− list.push(lsp::SymbolInformation {
365+ symbol: lsp::SymbolInformation {
332− name: symbol.name,
366+ name: symbol.name,
333− kind: symbol.kind,
367+ kind: symbol.kind,
334− tags: symbol.tags,
368+ tags: symbol.tags,
335− deprecated: symbol.deprecated,
369+ deprecated: symbol.deprecated,
336− location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
370+ location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
337− container_name: None,
371+ container_name: None,
372+ },
373+ offset_encoding,
338 });
374 });
339 for child in symbol.children.into_iter().flatten() {
375 for child in symbol.children.into_iter().flatten() {
340− nested_to_flat(list, file, child);
376+ nested_to_flat(list, file, child, offset_encoding);
341 }
377 }
342 }
378 }
343 let doc = doc!(cx.editor);
379 let doc = doc!(cx.editor);
344
380
345− let language_server = language_server!(cx.editor, doc);
381+ let mut seen_language_servers = HashSet::new();
346− let current_url = doc.url();
347− let offset_encoding = language_server.offset_encoding();
348
382
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| {
349− let future = match language_server.document_symbols(doc.identifier()) {
387+ let request = language_server.document_symbols(doc.identifier()).unwrap();
350− Some(future) => future,
388+ let offset_encoding = language_server.offset_encoding();
351− None => {
389+ let doc_id = doc.identifier();
352− cx.editor
390+
353− .set_error("Language server does not support document symbols");
391+ async move {
354− return;
392+ let json = request.await?;
355− }
356− };
357−
358− cx.callback(
359− future,
360− move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| {
393+ let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
361− if let Some(symbols) = response {
394+ let symbols = match response {
395+ Some(symbols) => symbols,
396+ None => return anyhow::Ok(vec![]),
397+ };
362 // lsp has two ways to represent symbols (flat/nested)
398 // lsp has two ways to represent symbols (flat/nested)
363 // convert the nested variant to flat, so that we have a homogeneous list
399 // convert the nested variant to flat, so that we have a homogeneous list
364 let symbols = match symbols {
400 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(),
366 lsp::DocumentSymbolResponse::Nested(symbols) => {
408 lsp::DocumentSymbolResponse::Nested(symbols) => {
367− let doc = doc!(editor);
368 let mut flat_symbols = Vec::new();
409 let mut flat_symbols = Vec::new();
369 for symbol in symbols {
410 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)
371 }
412 }
372 flat_symbols
413 flat_symbols
373 }
414 }
374 };
415 };
375−
416+ Ok(symbols)
376− let picker = sym_picker(symbols, current_url, offset_encoding);
377− compositor.push(Box::new(overlaid(picker)))
378 }
417 }
379− },
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)))
380− )
440+ });
381}
441}
382
442
383pub fn workspace_symbol_picker(cx: &mut Context) {
443pub fn workspace_symbol_picker(cx: &mut Context) {
384 let doc = doc!(cx.editor);
444 let doc = doc!(cx.editor);
385− let current_url = doc.url();
445+
386− let language_server = language_server!(cx.editor, doc);
446+ let get_symbols = move |pattern: String, editor: &mut Editor| {
387− let offset_encoding = language_server.offset_encoding();
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| {
388− let future = match language_server.workspace_symbols("".to_string()) {
453+ let request = language_server.workspace_symbols(pattern.clone()).unwrap();
389− Some(future) => future,
454+ let offset_encoding = language_server.offset_encoding();
390− None => {
455+ async move {
391− cx.editor
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() {
392− .set_error("Language server does not support workspace symbols");
474+ editor.set_error("No configured language server supports workspace symbols");
393− return;
394 }
475 }
395− };
396
476
397− cx.callback(
477+ async move {
398− future,
478+ let mut symbols = Vec::new();
399− move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
479+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
400− let symbols = response.unwrap_or_default();
480+ while let Some(mut lsp_items) = futures.try_next().await? {
401− let picker = sym_picker(symbols, current_url, offset_encoding);
481+ symbols.append(&mut lsp_items);
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− }
482+ }
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!(
483+ anyhow::Ok(symbols)
419− "Language server does not support workspace symbols"
420− ))
421− }
484+ }
422− .boxed();
485+ .boxed()
423− }
424− };
486+ };
425
487
426− let future = async move {
488+ let current_url = doc.url();
427− let json = symbol_request.await?;
489+ let initial_symbols = get_symbols("".to_owned(), cx.editor);
428− let response: Option<Vec<lsp::SymbolInformation>> =
429− serde_json::from_value(json)?;
430
490
431− Ok(response.unwrap_or_default())
491+ cx.jobs.callback(async move {
432− };
492+ let symbols = initial_symbols.await?;
433− future.boxed()
493+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
434− };
494+ let picker = sym_picker(symbols, current_url);
435 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
495 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
436 compositor.push(Box::new(overlaid(dyn_picker)))
496 compositor.push(Box::new(overlaid(dyn_picker)))
437− },
497+ };
498+
499+ Ok(Callback::EditorCompositor(Box::new(call)))
438− )
500+ });
439}
501}
440
502
441pub fn diagnostics_picker(cx: &mut Context) {
503pub fn diagnostics_picker(cx: &mut Context) {
442 let doc = doc!(cx.editor);
504 let doc = doc!(cx.editor);
443− let language_server = language_server!(cx.editor, doc);
444 if let Some(current_url) = doc.url() {
505 if let Some(current_url) = doc.url() {
445− let offset_encoding = language_server.offset_encoding();
446 let diagnostics = cx
506 let diagnostics = cx
447 .editor
507 .editor
448 .diagnostics
508 .diagnostics
@@ -454,7 +514,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
454 [(current_url.clone(), diagnostics)].into(),
514 [(current_url.clone(), diagnostics)].into(),
455 Some(current_url),
515 Some(current_url),
456 DiagnosticsFormat::HideSourcePath,
516 DiagnosticsFormat::HideSourcePath,
457− offset_encoding,
458 );
517 );
459 cx.push_layer(Box::new(overlaid(picker)));
518 cx.push_layer(Box::new(overlaid(picker)));
460 }
519 }
@@ -462,24 +521,27 @@ pub fn diagnostics_picker(cx: &mut Context) {
462
521
463pub fn workspace_diagnostics_picker(cx: &mut Context) {
522pub fn workspace_diagnostics_picker(cx: &mut Context) {
464 let doc = doc!(cx.editor);
523 let doc = doc!(cx.editor);
465− let language_server = language_server!(cx.editor, doc);
466 let current_url = doc.url();
524 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
468 let diagnostics = cx.editor.diagnostics.clone();
526 let diagnostics = cx.editor.diagnostics.clone();
469 let picker = diag_picker(
527 let picker = diag_picker(
470 cx,
528 cx,
471 diagnostics,
529 diagnostics,
472 current_url,
530 current_url,
473 DiagnosticsFormat::ShowSourcePath,
531 DiagnosticsFormat::ShowSourcePath,
474− offset_encoding,
475 );
532 );
476 cx.push_layer(Box::new(overlaid(picker)));
533 cx.push_layer(Box::new(overlaid(picker)));
477}
534}
478
535
536+struct CodeActionOrCommandItem {
537+ lsp_item: lsp::CodeActionOrCommand,
538+ language_server_id: usize,
539+}
540+
479−impl ui::menu::Item for lsp::CodeActionOrCommand {
541+impl ui::menu::Item for CodeActionOrCommandItem {
480 type Data = ();
542 type Data = ();
481 fn format(&self, _data: &Self::Data) -> Row {
543 fn format(&self, _data: &Self::Data) -> Row {
482− match self {
544+ match &self.lsp_item {
483 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
545 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
484 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
546 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
485 }
547 }
@@ -546,45 +608,42 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
546pub fn code_action(cx: &mut Context) {
608pub fn code_action(cx: &mut Context) {
547 let (view, doc) = current!(cx.editor);
609 let (view, doc) = current!(cx.editor);
548
610
549− let language_server = language_server!(cx.editor, doc);
550−
551 let selection_range = doc.selection(view.id).primary();
611 let selection_range = doc.selection(view.id).primary();
552− let offset_encoding = language_server.offset_encoding();
553
612
554− let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
613+ let mut seen_language_servers = HashSet::new();
555
614
556− let future = match language_server.code_actions(
615+ let mut futures: FuturesUnordered<_> = doc
557− doc.identifier(),
616+ .language_servers_with_feature(LanguageServerFeature::CodeAction)
558− range,
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);
559− // Filter and convert overlapping diagnostics
623+ // Filter and convert overlapping diagnostics
560− lsp::CodeActionContext {
624+ let code_action_context = lsp::CodeActionContext {
561− diagnostics: doc
625+ diagnostics: doc
562− .diagnostics()
626+ .diagnostics()
563− .iter()
627+ .iter()
564− .filter(|&diag| {
628+ .filter(|&diag| {
565− selection_range
629+ selection_range
566− .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
630+ .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
567− })
631+ })
568− .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
632+ .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
569− .collect(),
633+ .collect(),
570− only: None,
634+ only: None,
571− trigger_kind: Some(CodeActionTriggerKind::INVOKED),
635+ trigger_kind: Some(CodeActionTriggerKind::INVOKED),
572− },
636+ };
573− ) {
637+ let code_action_request =
574− Some(future) => future,
638+ language_server.code_actions(doc.identifier(), range, code_action_context)?;
575− None => {
639+ Some((code_action_request, language_server_id))
576− cx.editor
577− .set_error("Language server does not support code actions");
578− return;
579− }
640+ })
580− };
641+ .map(|(request, ls_id)| async move {
581−
642+ let json = request.await?;
582− cx.callback(
583− future,
584− move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
643+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
585 let mut actions = match response {
644 let mut actions = match response {
586 Some(a) => a,
645 Some(a) => a,
587− None => return,
646+ None => return anyhow::Ok(Vec::new()),
588 };
647 };
589
648
590 // remove disabled code actions
649 // remove disabled code actions
@@ -596,11 +655,6 @@ pub fn code_action(cx: &mut Context) {
596 )
655 )
597 });
656 });
598
657
599− if actions.is_empty() {
600− editor.set_status("No code actions available");
601− return;
602− }
603−
604 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
658 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
605 // Many details are modeled after vscode because language servers are usually tested against it.
659 // Many details are modeled after vscode because language servers are usually tested against it.
606 // VScode sorts the codeaction two times:
660 // VScode sorts the codeaction two times:
@@ -636,18 +690,51 @@ pub fn code_action(cx: &mut Context) {
636 .reverse()
690 .reverse()
637 });
691 });
638
692
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+ }
639− let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
721+ let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
640 if event != PromptEvent::Validate {
722 if event != PromptEvent::Validate {
641 return;
723 return;
642 }
724 }
643
725
644 // always present here
726 // 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();
646
733
647− match code_action {
734+ match &action.lsp_item {
648 lsp::CodeActionOrCommand::Command(command) => {
735 lsp::CodeActionOrCommand::Command(command) => {
649 log::debug!("code action command: {:?}", command);
736 log::debug!("code action command: {:?}", command);
650− execute_lsp_command(editor, command.clone());
737+ execute_lsp_command(editor, action.language_server_id, command.clone());
651 }
738 }
652 lsp::CodeActionOrCommand::CodeAction(code_action) => {
739 lsp::CodeActionOrCommand::CodeAction(code_action) => {
653 log::debug!("code action: {:?}", code_action);
740 log::debug!("code action: {:?}", code_action);
@@ -659,7 +746,7 @@ pub fn code_action(cx: &mut Context) {
659 // if code action provides both edit and command first the edit
746 // if code action provides both edit and command first the edit
660 // should be applied and then the command
747 // should be applied and then the command
661 if let Some(command) = &code_action.command {
748 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());
663 }
750 }
664 }
751 }
665 }
752 }
@@ -668,8 +755,10 @@ pub fn code_action(cx: &mut Context) {
668
755
669 let popup = Popup::new("code-action", picker).with_scrollbar(false);
756 let popup = Popup::new("code-action", picker).with_scrollbar(false);
670 compositor.replace_or_push("code-action", popup);
757 compositor.replace_or_push("code-action", popup);
671− },
758+ };
759+
760+ Ok(Callback::EditorCompositor(Box::new(call)))
672− )
761+ });
673}
762}
674
763
675impl ui::menu::Item for lsp::Command {
764impl ui::menu::Item for lsp::Command {
@@ -679,13 +768,13 @@ impl ui::menu::Item for lsp::Command {
679 }
768 }
680}
769}
681
770
682−pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
771+pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
683− let doc = doc!(editor);
684− let language_server = language_server!(editor, doc);
685−
686 // the command is executed on the server and communicated back
772 // the command is executed on the server and communicated back
687 // to the client asynchronously using workspace edits
773 // to the client asynchronously using workspace edits
774+ let future = match editor
775+ .language_server_by_id(language_server_id)
688− let future = match language_server.command(cmd) {
776+ .and_then(|language_server| language_server.command(cmd))
777+ {
689 Some(future) => future,
778 Some(future) => future,
690 None => {
779 None => {
691 editor.set_error("Language server does not support executing commands");
780 editor.set_error("Language server does not support executing commands");
@@ -977,21 +1066,17 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
977 }
1066 }
978}
1067}
979
1068
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+{
981 let (view, doc) = current!(cx.editor);
1074 let (view, doc) = current!(cx.editor);
982− let language_server = language_server!(cx.editor, doc);
983− let offset_encoding = language_server.offset_encoding();
984
1075
1076+ let language_server = language_server_with_feature!(cx.editor, doc, feature);
1077+ let offset_encoding = language_server.offset_encoding();
985 let pos = doc.position(view.id, offset_encoding);
1078 let pos = doc.position(view.id, offset_encoding);
986−
987− let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
1079+ let future = request_provider(language_server, pos, doc.identifier()).unwrap();
988− Some(future) => future,
989− None => {
990− cx.editor
991− .set_error("Language server does not support goto-declaration");
992− return;
993− }
994− };
995
1080
996 cx.callback(
1081 cx.callback(
997 future,
1082 future,
@@ -1002,102 +1087,56 @@ pub fn goto_declaration(cx: &mut Context) {
1002 );
1087 );
1003}
1088}
1004
1089
1005−pub fn goto_definition(cx: &mut Context) {
1090+pub fn goto_declaration(cx: &mut Context) {
1006− let (view, doc) = current!(cx.editor);
1091+ goto_single_impl(
1007− let language_server = language_server!(cx.editor, doc);
1092+ cx,
1008− let offset_encoding = language_server.offset_encoding();
1093+ LanguageServerFeature::GotoDeclaration,
1009−
1094+ |ls, pos, doc_id| ls.goto_declaration(doc_id, pos, None),
1010− let pos = doc.position(view.id, offset_encoding);
1095+ );
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− }
1096+}
1019− };
1020
1097
1021− cx.callback(
1098+pub fn goto_definition(cx: &mut Context) {
1022− future,
1099+ goto_single_impl(
1023− move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
1100+ cx,
1024− let items = to_locations(response);
1101+ LanguageServerFeature::GotoDefinition,
1025− goto_impl(editor, compositor, items, offset_encoding);
1102+ |ls, pos, doc_id| ls.goto_definition(doc_id, pos, None),
1026− },
1027 );
1103 );
1028}
1104}
1029
1105
1030pub fn goto_type_definition(cx: &mut Context) {
1106pub fn goto_type_definition(cx: &mut Context) {
1031− let (view, doc) = current!(cx.editor);
1107+ goto_single_impl(
1032− let language_server = language_server!(cx.editor, doc);
1108+ cx,
1033− let offset_encoding = language_server.offset_encoding();
1109+ LanguageServerFeature::GotoTypeDefinition,
1034−
1035− let pos = doc.position(view.id, offset_encoding);
1036−
1037− let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
1110+ |ls, pos, doc_id| ls.goto_type_definition(doc_id, 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− },
1052 );
1111 );
1053}
1112}
1054
1113
1055pub fn goto_implementation(cx: &mut Context) {
1114pub fn goto_implementation(cx: &mut Context) {
1056− let (view, doc) = current!(cx.editor);
1115+ goto_single_impl(
1057− let language_server = language_server!(cx.editor, doc);
1116+ cx,
1058− let offset_encoding = language_server.offset_encoding();
1117+ LanguageServerFeature::GotoImplementation,
1059−
1060− let pos = doc.position(view.id, offset_encoding);
1061−
1062− let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
1118+ |ls, pos, doc_id| ls.goto_implementation(doc_id, 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− },
1077 );
1119 );
1078}
1120}
1079
1121
1080pub fn goto_reference(cx: &mut Context) {
1122pub fn goto_reference(cx: &mut Context) {
1081 let config = cx.editor.config();
1123 let config = cx.editor.config();
1082 let (view, doc) = current!(cx.editor);
1124 let (view, doc) = current!(cx.editor);
1083− let language_server = language_server!(cx.editor, doc);
1084− let offset_encoding = language_server.offset_encoding();
1085
1125
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();
1086 let pos = doc.position(view.id, offset_encoding);
1131 let pos = doc.position(view.id, offset_encoding);
1087−
1088− let future = match language_server.goto_reference(
1132+ let future = language_server
1133+ .goto_reference(
1089− doc.identifier(),
1134+ doc.identifier(),
1090− pos,
1135+ pos,
1091− config.lsp.goto_reference_include_declaration,
1136+ config.lsp.goto_reference_include_declaration,
1092− None,
1137+ None,
1093− ) {
1138+ )
1094− Some(future) => future,
1139+ .unwrap();
1095− None => {
1096− cx.editor
1097− .set_error("Language server does not support goto-reference");
1098− return;
1099− }
1100− };
1101
1140
1102 cx.callback(
1141 cx.callback(
1103 future,
1142 future,
@@ -1108,7 +1147,7 @@ pub fn goto_reference(cx: &mut Context) {
1108 );
1147 );
1109}
1148}
1110
1149
1111−#[derive(PartialEq, Eq)]
1150+#[derive(PartialEq, Eq, Clone, Copy)]
1112pub enum SignatureHelpInvoked {
1151pub enum SignatureHelpInvoked {
1113 Manual,
1152 Manual,
1114 Automatic,
1153 Automatic,
@@ -1120,35 +1159,31 @@ pub fn signature_help(cx: &mut Context) {
1120
1159
1121pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1160pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1122 let (view, doc) = current!(cx.editor);
1161 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();
1138
1162
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| {
1139− let pos = doc.position(view.id, offset_encoding);
1167+ let pos = doc.position(view.id, language_server.offset_encoding());
1168+ language_server.text_document_signature_help(doc.identifier(), pos, None)
1169+ });
1140
1170
1141− let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
1171+ let Some(future) = future else {
1142− Some(f) => f,
1172+ // Do not show the message if signature help was invoked
1143− None => {
1173+ // automatically on backspace, trigger characters, etc.
1144− if was_manually_invoked {
1174+ if invoked == SignatureHelpInvoked::Manual {
1145− cx.editor
1146− .set_error("Language server does not support signature-help");
1175+ cx.editor.set_error("No configured language server supports signature-help");
1147− }
1148− return;
1149 }
1176 }
1177+ return;
1150 };
1178 };
1179+ signature_help_impl_with_future(cx, future.boxed(), invoked);
1180+}
1151
1181
1182+pub fn signature_help_impl_with_future(
1183+ cx: &mut Context,
1184+ future: BoxFuture<'static, helix_lsp::Result<Value>>,
1185+ invoked: SignatureHelpInvoked,
1186+) {
1152 cx.callback(
1187 cx.callback(
1153 future,
1188 future,
1154 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
1189 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@@ -1156,7 +1191,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1156
1191
1157 if !(config.lsp.auto_signature_help
1192 if !(config.lsp.auto_signature_help
1158 || SignatureHelp::visible_popup(compositor).is_some()
1193 || SignatureHelp::visible_popup(compositor).is_some()
1159− || was_manually_invoked)
1194+ || invoked == SignatureHelpInvoked::Manual)
1160 {
1195 {
1161 return;
1196 return;
1162 }
1197 }
@@ -1165,7 +1200,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1165 // it very probably means the server was a little slow to respond and the user has
1200 // it very probably means the server was a little slow to respond and the user has
1166 // already moved on to something else, making a signature help popup will just be an
1201 // already moved on to something else, making a signature help popup will just be an
1167 // annoyance, see https://github.com/helix-editor/helix/issues/3112
1202 // 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 {
1169 return;
1204 return;
1170 }
1205 }
1171
1206
@@ -1255,21 +1290,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1255
1290
1256pub fn hover(cx: &mut Context) {
1291pub fn hover(cx: &mut Context) {
1257 let (view, doc) = current!(cx.editor);
1292 let (view, doc) = current!(cx.editor);
1258− let language_server = language_server!(cx.editor, doc);
1259− let offset_encoding = language_server.offset_encoding();
1260
1293
1294+ // TODO support multiple language servers (merge UI somehow)
1295+ let language_server =
1296+ language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover);
1261 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
1297 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
1262−
1263− let pos = doc.position(view.id, offset_encoding);
1298+ let pos = doc.position(view.id, language_server.offset_encoding());
1264−
1299+ let future = language_server
1265− let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
1300+ .text_document_hover(doc.identifier(), pos, None)
1266− Some(future) => future,
1301+ .unwrap();
1267− None => {
1268− cx.editor
1269− .set_error("Language server does not support hover");
1270− return;
1271− }
1272− };
1273
1302
1274 cx.callback(
1303 cx.callback(
1275 future,
1304 future,
@@ -1349,7 +1378,11 @@ pub fn rename_symbol(cx: &mut Context) {
1349 }
1378 }
1350 }
1379 }
1351
1380
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> {
1353 let prompt = ui::Prompt::new(
1386 let prompt = ui::Prompt::new(
1354 "rename-to:".into(),
1387 "rename-to:".into(),
1355 None,
1388 None,
@@ -1358,22 +1391,22 @@ pub fn rename_symbol(cx: &mut Context) {
1358 if event != PromptEvent::Validate {
1391 if event != PromptEvent::Validate {
1359 return;
1392 return;
1360 }
1393 }
1361−
1362 let (view, doc) = current!(cx.editor);
1394 let (view, doc) = current!(cx.editor);
1363− let language_server = language_server!(cx.editor, doc);
1364− let offset_encoding = language_server.offset_encoding();
1365
1395
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();
1366 let pos = doc.position(view.id, offset_encoding);
1405 let pos = doc.position(view.id, offset_encoding);
1406+ let future = language_server
1407+ .rename_symbol(doc.identifier(), pos, input.to_string())
1408+ .unwrap();
1367
1409
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− };
1377 match block_on(future) {
1410 match block_on(future) {
1378 Ok(edits) => {
1411 Ok(edits) => {
1379 let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
1412 let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
@@ -1387,21 +1420,28 @@ pub fn rename_symbol(cx: &mut Context) {
1387 Box::new(prompt)
1420 Box::new(prompt)
1388 }
1421 }
1389
1422
1390− let (view, doc) = current!(cx.editor);
1423+ let (view, doc) = current_ref!(cx.editor);
1391− let language_server = language_server!(cx.editor, doc);
1424+
1392− let offset_encoding = language_server.offset_encoding();
1425+ let language_server_with_prepare_rename_support = doc
1393−
1426+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1394− if !language_server.supports_rename() {
1427+ .find(|ls| {
1395− cx.editor
1428+ matches!(
1396− .set_error("Language server does not support symbol renaming");
1429+ ls.capabilities().rename_provider,
1397− return;
1430+ Some(lsp::OneOf::Right(lsp::RenameOptions {
1431+ prepare_provider: Some(true),
1432+ ..
1398− }
1433+ }))
1399−
1434+ )
1400− let pos = doc.position(view.id, offset_encoding);
1435+ });
1401
1436
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
1402− match language_server.prepare_rename(doc.identifier(), pos) {
1442+ .prepare_rename(doc.identifier(), pos)
1403− // Language server supports textDocument/prepareRename, use it.
1443+ .unwrap();
1404− Some(future) => cx.callback(
1444+ cx.callback(
1405 future,
1445 future,
1406 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
1446 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
1407 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
1447 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1413,39 +1453,27 @@ pub fn rename_symbol(cx: &mut Context) {
1413 }
1453 }
1414 };
1454 };
1415
1455
1416− let prompt = create_rename_prompt(editor, prefill);
1456+ let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
1417
1457
1418 compositor.push(prompt);
1458 compositor.push(prompt);
1419 },
1459 },
1420− ),
1460+ );
1421− // Language server does not support textDocument/prepareRename, fall back
1461+ } else {
1422− // to word boundary selection.
1423− None => {
1424− let prefill = get_prefill_from_word_boundary(cx.editor);
1462+ let prefill = get_prefill_from_word_boundary(cx.editor);
1425−
1426− let prompt = create_rename_prompt(cx.editor, prefill);
1463+ let prompt = create_rename_prompt(cx.editor, prefill, None);
1427−
1428− cx.push_layer(prompt);
1464+ cx.push_layer(prompt);
1429− }
1430− };
1465+ }
1431}
1466}
1432
1467
1433pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1468pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1434 let (view, doc) = current!(cx.editor);
1469 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);
1436 let offset_encoding = language_server.offset_encoding();
1472 let offset_encoding = language_server.offset_encoding();
1437−
1438 let pos = doc.position(view.id, offset_encoding);
1473 let pos = doc.position(view.id, offset_encoding);
1439−
1474+ let future = language_server
1440− let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
1475+ .text_document_document_highlight(doc.identifier(), pos, None)
1441− {
1476+ .unwrap();
1442− Some(future) => future,
1443− None => {
1444− cx.editor
1445− .set_error("Language server does not support document highlight");
1446− return;
1447− }
1448− };
1449
1477
1450 cx.callback(
1478 cx.callback(
1451 future,
1479 future,
@@ -1455,8 +1483,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1455 _ => return,
1483 _ => return,
1456 };
1484 };
1457 let (view, doc) = current!(editor);
1485 let (view, doc) = current!(editor);
1458− let language_server = language_server!(editor, doc);
1459− let offset_encoding = language_server.offset_encoding();
1460 let text = doc.text();
1486 let text = doc.text();
1461 let pos = doc.selection(view.id).primary().head;
1487 let pos = doc.selection(view.id).primary().head;
1462
1488
@@ -1502,63 +1528,51 @@ fn compute_inlay_hints_for_view(
1502 let view_id = view.id;
1528 let view_id = view.id;
1503 let doc_id = view.doc;
1529 let doc_id = view.doc;
1504
1530
1505− let language_server = doc.language_server()?;
1531+ let language_server = doc
1506−
1532+ .language_servers_with_feature(LanguageServerFeature::InlayHints)
1507− let capabilities = language_server.capabilities();
1533+ .next()?;
1508−
1534+
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();
1535+ let doc_text = doc.text();
1515− let len_lines = doc_text.len_lines();
1536+ let len_lines = doc_text.len_lines();
1516−
1537+
1517− // Compute ~3 times the current view height of inlay hints, that way some scrolling
1538+ // 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
1539+ // 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
1540+ // than computing all the hints for the full file (which could be dozens of time
1520− // longer than the view is).
1541+ // longer than the view is).
1521− let view_height = view.inner_height();
1542+ 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()));
1543+ let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1524− let first_line = first_visible_line.saturating_sub(view_height);
1544+ let first_line = first_visible_line.saturating_sub(view_height);
1525− let last_line = first_visible_line
1545+ let last_line = first_visible_line
1526− .saturating_add(view_height.saturating_mul(2))
1546+ .saturating_add(view_height.saturating_mul(2))
1527− .min(len_lines);
1547+ .min(len_lines);
1528−
1548+
1529− let new_doc_inlay_hint_id = DocumentInlayHintsId {
1549+ let new_doc_inlay_hints_id = DocumentInlayHintsId {
1530− first_line,
1550+ first_line,
1531− last_line,
1551+ last_line,
1532− };
1552+ };
1533− // Don't recompute the annotations in case nothing has changed about the view
1553+ // Don't recompute the annotations in case nothing has changed about the view
1534− if !doc.inlay_hints_oudated
1554+ if !doc.inlay_hints_oudated
1535− && doc
1555+ && doc
1536− .inlay_hints(view_id)
1556+ .inlay_hints(view_id)
1537− .map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
1557+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
1538− {
1558+ {
1539− return None;
1559+ return None;
1540− }
1560+ }
1541
1561
1542− let doc_slice = doc_text.slice(..);
1562+ let doc_slice = doc_text.slice(..);
1543− let first_char_in_range = doc_slice.line_to_char(first_line);
1563+ 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);
1564+ let last_char_in_range = doc_slice.line_to_char(last_line);
1545
1565
1546− let range = helix_lsp::util::range_to_lsp_range(
1566+ let range = helix_lsp::util::range_to_lsp_range(
1547− doc_text,
1567+ doc_text,
1548− helix_core::Range::new(first_char_in_range, last_char_in_range),
1568+ helix_core::Range::new(first_char_in_range, last_char_in_range),
1549− language_server.offset_encoding(),
1569+ language_server.offset_encoding(),
1550− );
1570+ );
1551
1571
1552− (
1572+ let offset_encoding = language_server.offset_encoding();
1553− language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
1554− new_doc_inlay_hint_id,
1555− )
1556− }
1557− _ => return None,
1558− };
1559
1573
1560 let callback = super::make_job_callback(
1574 let callback = super::make_job_callback(
1561− future?,
1575+ language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
1562 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
1576 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
1563 // The config was modified or the window was closed while the request was in flight
1577 // The config was modified or the window was closed while the request was in flight
1564 if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
1578 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(
1572 };
1586 };
1573
1587
1574 // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
1588 // 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()) {
1589+ let mut hints = match response {
1576− (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
1590+ Some(hints) if !hints.is_empty() => hints,
1577 _ => {
1591 _ => {
1578 doc.set_inlay_hints(
1592 doc.set_inlay_hints(
1579 view_id,
1593 view_id,
helix-term/src/commands/typed.rs+41−40
@@ -1329,26 +1329,22 @@ fn lsp_workspace_command(
1329 if event != PromptEvent::Validate {
1329 if event != PromptEvent::Validate {
1330 return Ok(());
1330 return Ok(());
1331 }
1331 }
1332−
1333− let (_, doc) = current!(cx.editor);
1332+ let doc = doc!(cx.editor);
1334−
1333+ let Some((language_server_id, options)) = doc
1335− let language_server = match doc.language_server() {
1334+ .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
1336− Some(language_server) => language_server,
1335+ .find_map(|ls| {
1337− None => {
1336+ ls.capabilities()
1337+ .execute_command_provider
1338+ .as_ref()
1339+ .map(|options| (ls.id(), options))
1340+ })
1341+ else {
1338− cx.editor
1342+ cx.editor.set_status(
1339− .set_status("Language server not active for current buffer");
1343+ "No active language servers for this document support workspace commands",
1344+ );
1340− return Ok(());
1345+ return Ok(());
1341− }
1342 };
1346 };
1343
1347
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− };
1352 if args.is_empty() {
1348 if args.is_empty() {
1353 let commands = options
1349 let commands = options
1354 .commands
1350 .commands
@@ -1362,8 +1358,8 @@ fn lsp_workspace_command(
1362 let callback = async move {
1358 let callback = async move {
1363 let call: job::Callback = Callback::EditorCompositor(Box::new(
1359 let call: job::Callback = Callback::EditorCompositor(Box::new(
1364 move |_editor: &mut Editor, compositor: &mut Compositor| {
1360 move |_editor: &mut Editor, compositor: &mut Compositor| {
1365− let picker = ui::Picker::new(commands, (), |cx, command, _action| {
1361+ let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
1366− execute_lsp_command(cx.editor, command.clone());
1362+ execute_lsp_command(cx.editor, language_server_id, command.clone());
1367 });
1363 });
1368 compositor.push(Box::new(overlaid(picker)))
1364 compositor.push(Box::new(overlaid(picker)))
1369 },
1365 },
@@ -1376,6 +1372,7 @@ fn lsp_workspace_command(
1376 if options.commands.iter().any(|c| c == &command) {
1372 if options.commands.iter().any(|c| c == &command) {
1377 execute_lsp_command(
1373 execute_lsp_command(
1378 cx.editor,
1374 cx.editor,
1375+ language_server_id,
1379 helix_lsp::lsp::Command {
1376 helix_lsp::lsp::Command {
1380 title: command.clone(),
1377 title: command.clone(),
1381 arguments: None,
1378 arguments: None,
@@ -1407,7 +1404,6 @@ fn lsp_restart(
1407 .language_config()
1404 .language_config()
1408 .context("LSP not defined for the current document")?;
1405 .context("LSP not defined for the current document")?;
1409
1406
1410− let scope = config.scope.clone();
1411 cx.editor.language_servers.restart(
1407 cx.editor.language_servers.restart(
1412 config,
1408 config,
1413 doc.path(),
1409 doc.path(),
@@ -1420,13 +1416,22 @@ fn lsp_restart(
1420 .editor
1416 .editor
1421 .documents()
1417 .documents()
1422 .filter_map(|doc| match doc.language_config() {
1418 .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+ }
1424 _ => None,
1429 _ => None,
1425 })
1430 })
1426 .collect();
1431 .collect();
1427
1432
1428 for document_id in document_ids_to_refresh {
1433 for document_id in document_ids_to_refresh {
1429− cx.editor.refresh_language_server(document_id);
1434+ cx.editor.refresh_language_servers(document_id);
1430 }
1435 }
1431
1436
1432 Ok(())
1437 Ok(())
@@ -1441,22 +1446,18 @@ fn lsp_stop(
1441 return Ok(());
1446 return Ok(());
1442 }
1447 }
1443
1448
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<_>>();
1445
1453
1446− let ls_id = doc
1454+ for ls_name in &ls_shutdown_names {
1447− .language_server()
1455+ cx.editor.language_servers.stop(ls_name);
1448− .map(|ls| ls.id())
1449− .context("LSP not running for the current document")?;
1450
1456
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+ for doc in cx.editor.documents_mut() {
1457− if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
1458+ if let Some(client) = doc.remove_language_server_by_name(ls_name) {
1458− doc.set_language_server(None);
1459+ doc.clear_diagnostics(client.id());
1459− doc.set_diagnostics(Default::default());
1460+ }
1460 }
1461 }
1461 }
1462 }
1462
1463
@@ -1850,7 +1851,7 @@ fn language(
1850 doc.detect_indent_and_line_ending();
1851 doc.detect_indent_and_line_ending();
1851
1852
1852 let id = doc.id();
1853 let id = doc.id();
1853− cx.editor.refresh_language_server(id);
1854+ cx.editor.refresh_language_servers(id);
1854 Ok(())
1855 Ok(())
1855}
1856}
1856
1857
@@ -2588,14 +2589,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2588 TypableCommand {
2589 TypableCommand {
2589 name: "lsp-restart",
2590 name: "lsp-restart",
2590 aliases: &[],
2591 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",
2592 fun: lsp_restart,
2593 fun: lsp_restart,
2593 signature: CommandSignature::none(),
2594 signature: CommandSignature::none(),
2594 },
2595 },
2595 TypableCommand {
2596 TypableCommand {
2596 name: "lsp-stop",
2597 name: "lsp-stop",
2597 aliases: &[],
2598 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",
2599 fun: lsp_stop,
2600 fun: lsp_stop,
2600 signature: CommandSignature::none(),
2601 signature: CommandSignature::none(),
2601 },
2602 },
helix-term/src/health.rs+15−7
@@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> {
192 for lang in &syn_loader_conf.language {
192 for lang in &syn_loader_conf.language {
193 column(&lang.language_id, Color::Reset);
193 column(&lang.language_id, Color::Reset);
194
194
195− let lsp = lang
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
196− .language_server
199+ .language_server
197− .as_ref()
200+ .get(&ls.name)
198− .map(|lsp| lsp.command.to_string());
201+ .map(|config| config.command.clone())
202+ });
199 check_binary(lsp);
203 check_binary(lsp);
200
204
201 let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
205 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<()> {
264 }
268 }
265 };
269 };
266
270
271+ // TODO multiple language servers
267 probe_protocol(
272 probe_protocol(
268 "language server",
273 "language server",
274+ lang.language_servers.first().and_then(|ls| {
275+ syn_loader_conf
269− lang.language_server
276+ .language_server
270− .as_ref()
277+ .get(&ls.name)
271− .map(|lsp| lsp.command.to_string()),
278+ .map(|config| config.command.clone())
279+ }),
272 )?;
280 )?;
273
281
274 probe_protocol(
282 probe_protocol(
helix-term/src/ui/completion.rs+53−48
@@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
15use crate::commands;
15use crate::commands;
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
17
17
18−use helix_lsp::{lsp, util};
18+use helix_lsp::{lsp, util, OffsetEncoding};
19
19
20impl menu::Item for CompletionItem {
20impl menu::Item for CompletionItem {
21 type Data = ();
21 type Data = ();
@@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
38 || self.item.tags.as_ref().map_or(false, |tags| {
38 || self.item.tags.as_ref().map_or(false, |tags| {
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
40 });
40 });
41+
41 menu::Row::new(vec![
42 menu::Row::new(vec![
42 menu::Cell::from(Span::styled(
43 menu::Cell::from(Span::styled(
43 self.item.label.as_str(),
44 self.item.label.as_str(),
@@ -79,19 +80,15 @@ impl menu::Item for CompletionItem {
79 }
80 }
80 None => "",
81 None => "",
81 }),
82 }),
82− // self.detail.as_deref().unwrap_or("")
83− // self.label_details
84− // .as_ref()
85− // .or(self.detail())
86− // .as_str(),
87 ])
83 ])
88 }
84 }
89}
85}
90
86
91#[derive(Debug, PartialEq, Default, Clone)]
87#[derive(Debug, PartialEq, Default, Clone)]
92−struct CompletionItem {
88+pub struct CompletionItem {
93− item: lsp::CompletionItem,
89+ pub item: lsp::CompletionItem,
90+ pub language_server_id: usize,
94− resolved: bool,
91+ pub resolved: bool,
95}
92}
96
93
97/// Wraps a Menu.
94/// Wraps a Menu.
@@ -109,29 +106,21 @@ impl Completion {
109 pub fn new(
106 pub fn new(
110 editor: &Editor,
107 editor: &Editor,
111 savepoint: Arc<SavePoint>,
108 savepoint: Arc<SavePoint>,
112− mut items: Vec<lsp::CompletionItem>,
109+ mut items: Vec<CompletionItem>,
113− offset_encoding: helix_lsp::OffsetEncoding,
114 start_offset: usize,
110 start_offset: usize,
115 trigger_offset: usize,
111 trigger_offset: usize,
116 ) -> Self {
112 ) -> Self {
117 let replace_mode = editor.config().completion_replace;
113 let replace_mode = editor.config().completion_replace;
118 // Sort completion items according to their preselect status (given by the LSP server)
114 // Sort completion items according to their preselect status (given by the LSP server)
119− items.sort_by_key(|item| !item.preselect.unwrap_or(false));
115+ items.sort_by_key(|item| !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();
127
116
128 // Then create the menu
117 // Then create the menu
129 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
118 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
130 fn item_to_transaction(
119 fn item_to_transaction(
131 doc: &Document,
120 doc: &Document,
132 view_id: ViewId,
121 view_id: ViewId,
133− item: &CompletionItem,
122+ item: &lsp::CompletionItem,
134− offset_encoding: helix_lsp::OffsetEncoding,
123+ offset_encoding: OffsetEncoding,
135 trigger_offset: usize,
124 trigger_offset: usize,
136 include_placeholder: bool,
125 include_placeholder: bool,
137 replace_mode: bool,
126 replace_mode: bool,
@@ -141,7 +130,7 @@ impl Completion {
141 let text = doc.text().slice(..);
130 let text = doc.text().slice(..);
142 let primary_cursor = selection.primary().cursor(text);
131 let primary_cursor = selection.primary().cursor(text);
143
132
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 {
145 let edit = match edit {
134 let edit = match edit {
146 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
135 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
147 lsp::CompletionTextEdit::InsertAndReplace(item) => {
136 lsp::CompletionTextEdit::InsertAndReplace(item) => {
@@ -164,10 +153,9 @@ impl Completion {
164 (Some((start_offset, end_offset)), edit.new_text)
153 (Some((start_offset, end_offset)), edit.new_text)
165 } else {
154 } else {
166 let new_text = item
155 let new_text = item
167− .item
168 .insert_text
156 .insert_text
169 .clone()
157 .clone()
170− .unwrap_or_else(|| item.item.label.clone());
158+ .unwrap_or_else(|| item.label.clone());
171 // check that we are still at the correct savepoint
159 // check that we are still at the correct savepoint
172 // we can still generate a transaction regardless but if the
160 // we can still generate a transaction regardless but if the
173 // document changed (and not just the selection) then we will
161 // document changed (and not just the selection) then we will
@@ -176,9 +164,9 @@ impl Completion {
176 (None, new_text)
164 (None, new_text)
177 };
165 };
178
166
179− if matches!(item.item.kind, Some(lsp::CompletionItemKind::SNIPPET))
167+ if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
180 || matches!(
168 || matches!(
181− item.item.insert_text_format,
169+ item.insert_text_format,
182 Some(lsp::InsertTextFormat::SNIPPET)
170 Some(lsp::InsertTextFormat::SNIPPET)
183 )
171 )
184 {
172 {
@@ -223,6 +211,23 @@ impl Completion {
223
211
224 let (view, doc) = current!(editor);
212 let (view, doc) = current!(editor);
225
213
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+
226 match event {
231 match event {
227 PromptEvent::Abort => {}
232 PromptEvent::Abort => {}
228 PromptEvent::Update => {
233 PromptEvent::Update => {
@@ -250,8 +255,8 @@ impl Completion {
250 let transaction = item_to_transaction(
255 let transaction = item_to_transaction(
251 doc,
256 doc,
252 view.id,
257 view.id,
253− item,
258+ &item.item,
254− offset_encoding,
259+ language_server!(item).offset_encoding(),
255 trigger_offset,
260 trigger_offset,
256 true,
261 true,
257 replace_mode,
262 replace_mode,
@@ -267,10 +272,18 @@ impl Completion {
267 // always present here
272 // always present here
268 let mut item = item.unwrap().clone();
273 let mut item = item.unwrap().clone();
269
274
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+
270 // resolve item if not yet resolved
283 // resolve item if not yet resolved
271 if !item.resolved {
284 if !item.resolved {
272 if let Some(resolved) =
285 if let Some(resolved) =
273− Self::resolve_completion_item(doc, item.item.clone())
286+ Self::resolve_completion_item(language_server, item.item.clone())
274 {
287 {
275 item.item = resolved;
288 item.item = resolved;
276 }
289 }
@@ -280,7 +293,7 @@ impl Completion {
280 let transaction = item_to_transaction(
293 let transaction = item_to_transaction(
281 doc,
294 doc,
282 view.id,
295 view.id,
283− &item,
296+ &item.item,
284 offset_encoding,
297 offset_encoding,
285 trigger_offset,
298 trigger_offset,
286 false,
299 false,
@@ -323,11 +336,9 @@ impl Completion {
323 }
336 }
324
337
325 fn resolve_completion_item(
338 fn resolve_completion_item(
326− doc: &Document,
339+ language_server: &helix_lsp::Client,
327 completion_item: lsp::CompletionItem,
340 completion_item: lsp::CompletionItem,
328 ) -> Option<lsp::CompletionItem> {
341 ) -> Option<lsp::CompletionItem> {
329− let language_server = doc.language_server()?;
330−
331 let future = language_server.resolve_completion_item(completion_item)?;
342 let future = language_server.resolve_completion_item(completion_item)?;
332 let response = helix_lsp::block_on(future);
343 let response = helix_lsp::block_on(future);
333 match response {
344 match response {
@@ -398,16 +409,10 @@ impl Completion {
398 _ => return false,
409 _ => return false,
399 };
410 };
400
411
401− let language_server = match doc!(cx.editor).language_server() {
412+ let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
402− Some(language_server) => language_server,
403− None => return false,
404− };
405
413
406 // This method should not block the compositor so we handle the response asynchronously.
414 // 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()) {
415+ let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
408− Some(future) => future,
409− None => return false,
410− };
411
416
412 cx.callback(
417 cx.callback(
413 future,
418 future,
@@ -422,13 +427,13 @@ impl Completion {
422 .unwrap()
427 .unwrap()
423 .completion
428 .completion
424 {
429 {
425− completion.replace_item(
426− current_item,
427− CompletionItem {
430+ let resolved_item = CompletionItem {
428− item: resolved_item,
431+ item: resolved_item,
432+ language_server_id: current_item.language_server_id,
429− resolved: true,
433+ resolved: true,
430− },
434+ };
431− );
435+
436+ completion.replace_item(current_item, resolved_item);
432 }
437 }
433 },
438 },
434 );
439 );
helix-term/src/ui/editor.rs+5−12
@@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
33
33
34use tui::{buffer::Buffer as Surface, text::Span};
34use tui::{buffer::Buffer as Surface, text::Span};
35
35
36−use super::statusline;
36+use super::{completion::CompletionItem, statusline};
37use super::{document::LineDecoration, lsp::SignatureHelp};
37use super::{document::LineDecoration, lsp::SignatureHelp};
38
38
39pub struct EditorView {
39pub struct EditorView {
@@ -650,7 +650,7 @@ impl EditorView {
650 .primary()
650 .primary()
651 .cursor(doc.text().slice(..));
651 .cursor(doc.text().slice(..));
652
652
653− let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
653+ let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
655 });
655 });
656
656
@@ -953,20 +953,13 @@ impl EditorView {
953 &mut self,
953 &mut self,
954 editor: &mut Editor,
954 editor: &mut Editor,
955 savepoint: Arc<SavePoint>,
955 savepoint: Arc<SavePoint>,
956− items: Vec<helix_lsp::lsp::CompletionItem>,
956+ items: Vec<CompletionItem>,
957− offset_encoding: helix_lsp::OffsetEncoding,
958 start_offset: usize,
957 start_offset: usize,
959 trigger_offset: usize,
958 trigger_offset: usize,
960 size: Rect,
959 size: Rect,
961 ) -> Option<Rect> {
960 ) -> Option<Rect> {
962− let mut completion = Completion::new(
961+ let mut completion =
963− editor,
962+ Completion::new(editor, savepoint, items, start_offset, trigger_offset);
964− savepoint,
965− items,
966− offset_encoding,
967− start_offset,
968− trigger_offset,
969− );
970
963
971 if completion.is_empty() {
964 if completion.is_empty() {
972 // skip if we got no completion results
965 // skip if we got no completion results
helix-term/src/ui/mod.rs+7−15
@@ -17,7 +17,7 @@ mod text;
17use crate::compositor::{Component, Compositor};
17use crate::compositor::{Component, Compositor};
18use crate::filter_picker_entry;
18use crate::filter_picker_entry;
19use crate::job::{self, Callback};
19use crate::job::{self, Callback};
20−pub use completion::Completion;
20+pub use completion::{Completion, CompletionItem};
21pub use editor::EditorView;
21pub use editor::EditorView;
22pub use markdown::Markdown;
22pub use markdown::Markdown;
23pub use menu::Menu;
23pub use menu::Menu;
@@ -238,6 +238,7 @@ pub mod completers {
238 use crate::ui::prompt::Completion;
238 use crate::ui::prompt::Completion;
239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
240 use fuzzy_matcher::FuzzyMatcher;
240 use fuzzy_matcher::FuzzyMatcher;
241+ use helix_core::syntax::LanguageServerFeature;
241 use helix_view::document::SCRATCH_BUFFER_NAME;
242 use helix_view::document::SCRATCH_BUFFER_NAME;
242 use helix_view::theme;
243 use helix_view::theme;
243 use helix_view::{editor::Config, Editor};
244 use helix_view::{editor::Config, Editor};
@@ -393,20 +394,11 @@ pub mod completers {
393 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394 let matcher = Matcher::default();
395 let matcher = Matcher::default();
395
396
396− let (_, doc) = current_ref!(editor);
397+ let Some(options) = doc!(editor)
397−
398+ .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
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 {
399+ .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
406− Some(options) => options,
400+ else {
407− None => {
408− return vec![];
401+ return vec![];
409− }
410 };
402 };
411
403
412 let mut matches: Vec<_> = options
404 let mut matches: Vec<_> = options
helix-term/src/ui/statusline.rs+5−6
@@ -197,15 +197,15 @@ where
197 );
197 );
198}
198}
199
199
200+// TODO think about handling multiple language servers
200fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201where
202where
202 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203{
204{
205+ let language_server = context.doc.language_servers().next();
204 write(
206 write(
205 context,
207 context,
206− context
207− .doc
208− .language_server()
208+ language_server
209 .and_then(|srv| {
209 .and_then(|srv| {
210 context
210 context
211 .spinners
211 .spinners
@@ -225,8 +225,7 @@ where
225{
225{
226 let (warnings, errors) = context
226 let (warnings, errors) = context
227 .doc
227 .doc
228− .diagnostics()
228+ .shown_diagnostics()
229− .iter()
230 .fold((0, 0), |mut counts, diag| {
229 .fold((0, 0), |mut counts, diag| {
231 use helix_core::diagnostic::Severity;
230 use helix_core::diagnostic::Severity;
232 match diag.severity {
231 match diag.severity {
@@ -266,7 +265,7 @@ where
266 .diagnostics
265 .diagnostics
267 .values()
266 .values()
268 .flatten()
267 .flatten()
269− .fold((0, 0), |mut counts, diag| {
268+ .fold((0, 0), |mut counts, (diag, _)| {
270 match diag.severity {
269 match diag.severity {
271 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
270 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
272 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
271 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
helix-view/src/document.rs+77−35
@@ -6,7 +6,7 @@ use futures_util::FutureExt;
6use helix_core::auto_pairs::AutoPairs;
6use helix_core::auto_pairs::AutoPairs;
7use helix_core::doc_formatter::TextFormat;
7use helix_core::doc_formatter::TextFormat;
8use helix_core::encoding::Encoding;
8use helix_core::encoding::Encoding;
9−use helix_core::syntax::Highlight;
9+use helix_core::syntax::{Highlight, LanguageServerFeature};
10use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
10use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
11use helix_core::Range;
11use helix_core::Range;
12use helix_vcs::{DiffHandle, DiffProviderRegistry};
12use helix_vcs::{DiffHandle, DiffProviderRegistry};
@@ -179,8 +179,8 @@ pub struct Document {
179 version: i32, // should be usize?
179 version: i32, // should be usize?
180 pub(crate) modified_since_accessed: bool,
180 pub(crate) modified_since_accessed: bool,
181
181
182− diagnostics: Vec<Diagnostic>,
182+ pub(crate) diagnostics: Vec<Diagnostic>,
183− language_server: Option<Arc<helix_lsp::Client>>,
183+ pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
184
184
185 diff_handle: Option<DiffHandle>,
185 diff_handle: Option<DiffHandle>,
186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@@ -580,7 +580,7 @@ where
580 *mut_ref = f(mem::take(mut_ref));
580 *mut_ref = f(mem::take(mut_ref));
581}
581}
582
582
583−use helix_lsp::lsp;
583+use helix_lsp::{lsp, Client, LanguageServerName};
584use url::Url;
584use url::Url;
585
585
586impl Document {
586impl Document {
@@ -616,7 +616,7 @@ impl Document {
616 last_saved_time: SystemTime::now(),
616 last_saved_time: SystemTime::now(),
617 last_saved_revision: 0,
617 last_saved_revision: 0,
618 modified_since_accessed: false,
618 modified_since_accessed: false,
619− language_server: None,
619+ language_servers: HashMap::new(),
620 diff_handle: None,
620 diff_handle: None,
621 config,
621 config,
622 version_control_head: None,
622 version_control_head: None,
@@ -730,10 +730,12 @@ impl Document {
730 return Some(formatting_future.boxed());
730 return Some(formatting_future.boxed());
731 };
731 };
732
732
733− let language_server = self.language_server()?;
734 let text = self.text.clone();
733 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()?;
735 let offset_encoding = language_server.offset_encoding();
738 let offset_encoding = language_server.offset_encoding();
736−
737 let request = language_server.text_document_formatting(
739 let request = language_server.text_document_formatting(
738 self.identifier(),
740 self.identifier(),
739 lsp::FormattingOptions {
741 lsp::FormattingOptions {
@@ -797,13 +799,12 @@ impl Document {
797 if self.path.is_none() {
799 if self.path.is_none() {
798 bail!("Can't save with no path set!");
800 bail!("Can't save with no path set!");
799 }
801 }
800−
801 self.path.as_ref().unwrap().clone()
802 self.path.as_ref().unwrap().clone()
802 }
803 }
803 };
804 };
804
805
805 let identifier = self.path().map(|_| self.identifier());
806 let identifier = self.path().map(|_| self.identifier());
806− let language_server = self.language_server.clone();
807+ let language_servers = self.language_servers.clone();
807
808
808 // mark changes up to now as saved
809 // mark changes up to now as saved
809 let current_rev = self.get_current_revision();
810 let current_rev = self.get_current_revision();
@@ -847,14 +848,13 @@ impl Document {
847 text: text.clone(),
848 text: text.clone(),
848 };
849 };
849
850
850− if let Some(language_server) = language_server {
851+ for (_, language_server) in language_servers {
851 if !language_server.is_initialized() {
852 if !language_server.is_initialized() {
852 return Ok(event);
853 return Ok(event);
853 }
854 }
854−
855− if let Some(identifier) = identifier {
855+ if let Some(identifier) = &identifier {
856 if let Some(notification) =
856 if let Some(notification) =
857− language_server.text_document_did_save(identifier, &text)
857+ language_server.text_document_did_save(identifier.clone(), &text)
858 {
858 {
859 notification.await?;
859 notification.await?;
860 }
860 }
@@ -1004,11 +1004,6 @@ impl Document {
1004 Ok(())
1004 Ok(())
1005 }
1005 }
1006
1006
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−
1012 /// Select text within the [`Document`].
1007 /// Select text within the [`Document`].
1013 pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
1008 pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
1014 // TODO: use a transaction?
1009 // TODO: use a transaction?
@@ -1159,7 +1154,7 @@ impl Document {
1159
1154
1160 if emit_lsp_notification {
1155 if emit_lsp_notification {
1161 // emit lsp notification
1156 // emit lsp notification
1162− if let Some(language_server) = self.language_server() {
1157+ for language_server in self.language_servers() {
1163 let notify = language_server.text_document_did_change(
1158 let notify = language_server.text_document_did_change(
1164 self.versioned_identifier(),
1159 self.versioned_identifier(),
1165 &old_doc,
1160 &old_doc,
@@ -1415,18 +1410,13 @@ impl Document {
1415 .map(|language| language.language_id.as_str())
1410 .map(|language| language.language_id.as_str())
1416 }
1411 }
1417
1412
1418− /// Language ID for the document. Either the `language-id` from the
1413+ /// Language ID for the document. Either the `language-id`,
1419− /// `language-server` configuration, or the document language if no
1420− /// `language-id` has been specified.
1414+ /// or the document language name if no `language-id` has been specified.
1421 pub fn language_id(&self) -> Option<&str> {
1415 pub fn language_id(&self) -> Option<&str> {
1422− let language_config = self.language.as_deref()?;
1423−
1424− language_config
1416+ self.language_config()?
1425− .language_server
1417+ .language_server_language_id
1426− .as_ref()?
1427− .language_id
1428 .as_deref()
1418 .as_deref()
1429− .or(Some(language_config.language_id.as_str()))
1419+ .or_else(|| self.language_name())
1430 }
1420 }
1431
1421
1432 /// Corresponding [`LanguageConfiguration`].
1422 /// Corresponding [`LanguageConfiguration`].
@@ -1439,10 +1429,45 @@ impl Document {
1439 self.version
1429 self.version
1440 }
1430 }
1441
1431
1442− /// Language server if it has been initialized.
1432+ /// maintains the order as configured in the language_servers TOML array
1443− pub fn language_server(&self) -> Option<&helix_lsp::Client> {
1433+ pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
1444− let server = self.language_server.as_deref()?;
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)?;
1445− server.is_initialized().then_some(server)
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)
1446 }
1471 }
1447
1472
1448 pub fn diff_handle(&self) -> Option<&DiffHandle> {
1473 pub fn diff_handle(&self) -> Option<&DiffHandle> {
@@ -1565,12 +1590,29 @@ impl Document {
1565 &self.diagnostics
1590 &self.diagnostics
1566 }
1591 }
1567
1592
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,
1568− pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
1602+ mut diagnostics: Vec<Diagnostic>,
1603+ language_server_id: usize,
1604+ ) {
1605+ self.clear_diagnostics(language_server_id);
1569− self.diagnostics = diagnostics;
1606+ self.diagnostics.append(&mut diagnostics);
1570 self.diagnostics
1607 self.diagnostics
1571 .sort_unstable_by_key(|diagnostic| diagnostic.range);
1608 .sort_unstable_by_key(|diagnostic| diagnostic.range);
1572 }
1609 }
1573
1610
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+
1574 /// Get the document's auto pairs. If the document has a recognized
1616 /// Get the document's auto pairs. If the document has a recognized
1575 /// language config with auto pairs configured, returns that;
1617 /// language config with auto pairs configured, returns that;
1576 /// otherwise, falls back to the global auto pairs config. If the global
1618 /// 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 {
818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
819 pub macro_replaying: Vec<char>,
819 pub macro_replaying: Vec<char>,
820 pub language_servers: helix_lsp::Registry,
820 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)>>,
822 pub diff_providers: DiffProviderRegistry,
822 pub diff_providers: DiffProviderRegistry,
823
823
824 pub debugger: Option<dap::Client>,
824 pub debugger: Option<dap::Client>,
@@ -874,7 +874,7 @@ pub struct Editor {
874 /// times during rendering and should not be set by other functions.
874 /// times during rendering and should not be set by other functions.
875 pub cursor_cache: Cell<Option<Option<Position>>>,
875 pub cursor_cache: Cell<Option<Option<Position>>>,
876 /// When a new completion request is sent to the server old
876 /// 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
878 /// request is associated with a channel that cancels
878 /// request is associated with a channel that cancels
879 /// when the channel is dropped. That channel is stored
879 /// when the channel is dropped. That channel is stored
880 /// here. When a new completion request is sent this
880 /// here. When a new completion request is sent this
@@ -941,6 +941,7 @@ impl Editor {
941 syn_loader: Arc<syntax::Loader>,
941 syn_loader: Arc<syntax::Loader>,
942 config: Arc<dyn DynAccess<Config>>,
942 config: Arc<dyn DynAccess<Config>>,
943 ) -> Self {
943 ) -> Self {
944+ let language_servers = helix_lsp::Registry::new(syn_loader.clone());
944 let conf = config.load();
945 let conf = config.load();
945 let auto_pairs = (&conf.auto_pairs).into();
946 let auto_pairs = (&conf.auto_pairs).into();
946
947
@@ -960,7 +961,7 @@ impl Editor {
960 macro_recording: None,
961 macro_recording: None,
961 macro_replaying: Vec::new(),
962 macro_replaying: Vec::new(),
962 theme: theme_loader.default(),
963 theme: theme_loader.default(),
963− language_servers: helix_lsp::Registry::new(),
964+ language_servers,
964 diagnostics: BTreeMap::new(),
965 diagnostics: BTreeMap::new(),
965 diff_providers: DiffProviderRegistry::default(),
966 diff_providers: DiffProviderRegistry::default(),
966 debugger: None,
967 debugger: None,
@@ -1092,60 +1093,75 @@ impl Editor {
1092 self._refresh();
1093 self._refresh();
1093 }
1094 }
1094
1095
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+
1095 /// Refreshes the language server for a given document
1101 /// Refreshes the language server for a given document
1096− pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1102+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1097− self.launch_language_server(doc_id)
1103+ self.launch_language_servers(doc_id)
1098 }
1104 }
1099
1105
1100 /// Launch a language server for a given document
1106 /// 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<()> {
1102 if !self.config().lsp.enable {
1108 if !self.config().lsp.enable {
1103 return None;
1109 return None;
1104 }
1110 }
1105−
1106 // if doc doesn't have a URL it's a scratch buffer, ignore it
1111 // 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()?;
1108 let (lang, path) = (doc.language.clone(), doc.path().cloned());
1114 let (lang, path) = (doc.language.clone(), doc.path().cloned());
1109 let config = doc.config.load();
1115 let config = doc.config.load();
1110 let root_dirs = &config.workspace_lsp_roots;
1116 let root_dirs = &config.workspace_lsp_roots;
1111
1117
1112− // try to find a language server based on the language name
1118+ // try to find language servers based on the language name
1113− let language_server = lang.as_ref().and_then(|language| {
1119+ let language_servers = lang.as_ref().and_then(|language| {
1114 self.language_servers
1120 self.language_servers
1115 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1121 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1116 .map_err(|e| {
1122 .map_err(|e| {
1117 log::error!(
1123 log::error!(
1118− "Failed to initialize the LSP for `{}` {{ {} }}",
1124+ "Failed to initialize the language servers for `{}` {{ {} }}",
1119 language.scope(),
1125 language.scope(),
1120 e
1126 e
1121 )
1127 )
1122 })
1128 })
1123 .ok()
1129 .ok()
1124− .flatten()
1125 });
1130 });
1126
1131
1127− let doc = self.document_mut(doc_id)?;
1132+ if let Some(language_servers) = language_servers {
1128− let doc_url = doc.url()?;
1133+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1129
1134
1130− if let Some(language_server) = language_server {
1131− // only spawn a new lang server if the servers aren't the same
1135+ // only spawn new language servers 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− }
1136
1136
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+ });
1138
1143
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 {
1139 // TODO: this now races with on_init code if the init happens too quickly
1155 // TODO: this now races with on_init code if the init happens too quickly
1140 tokio::spawn(language_server.text_document_did_open(
1156 tokio::spawn(language_server.text_document_did_open(
1141− doc_url,
1157+ doc_url.clone(),
1142 doc.version(),
1158 doc.version(),
1143 doc.text(),
1159 doc.text(),
1144− language_id,
1160+ language_id.clone(),
1145 ));
1161 ));
1146−
1147− doc.set_language_server(Some(language_server));
1148 }
1162 }
1163+
1164+ doc.language_servers = language_servers;
1149 }
1165 }
1150 Some(())
1166 Some(())
1151 }
1167 }
@@ -1338,7 +1354,7 @@ impl Editor {
1338 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1354 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1339
1355
1340 let id = self.new_document(doc);
1356 let id = self.new_document(doc);
1341− let _ = self.launch_language_server(id);
1357+ let _ = self.launch_language_servers(id);
1342
1358
1343 id
1359 id
1344 };
1360 };
@@ -1368,7 +1384,7 @@ impl Editor {
1368 // This will also disallow any follow-up writes
1384 // This will also disallow any follow-up writes
1369 self.saves.remove(&doc_id);
1385 self.saves.remove(&doc_id);
1370
1386
1371− if let Some(language_server) = doc.language_server() {
1387+ for language_server in doc.language_servers() {
1372 // TODO: track error
1388 // TODO: track error
1373 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1389 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1374 }
1390 }
helix-view/src/gutter.rs+17−19
@@ -1,5 +1,7 @@
1use std::fmt::Write;
1use std::fmt::Write;
2
2
3+use helix_core::syntax::LanguageServerFeature;
4+
3use crate::{
5use crate::{
4 editor::GutterType,
6 editor::GutterType,
5 graphics::{Style, UnderlineStyle},
7 graphics::{Style, UnderlineStyle},
@@ -55,7 +57,7 @@ pub fn diagnostic<'doc>(
55 let error = theme.get("error");
57 let error = theme.get("error");
56 let info = theme.get("info");
58 let info = theme.get("info");
57 let hint = theme.get("hint");
59 let hint = theme.get("hint");
58− let diagnostics = doc.diagnostics();
60+ let diagnostics = &doc.diagnostics;
59
61
60 Box::new(
62 Box::new(
61 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
63 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
@@ -63,28 +65,24 @@ pub fn diagnostic<'doc>(
63 return None;
65 return None;
64 }
66 }
65 use helix_core::diagnostic::Severity;
67 use helix_core::diagnostic::Severity;
66− if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
68+ let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line);
67− let after = diagnostics[index..].iter().take_while(|d| d.line == line);
69+ let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..]
68−
70+ .iter()
69− let before = diagnostics[..index]
71+ .take_while(|d| {
70− .iter()
72+ d.line == line
71− .rev()
73+ && doc
72− .take_while(|d| d.line == line);
74+ .language_servers_with_feature(LanguageServerFeature::Diagnostics)
73−
75+ .any(|ls| ls.id() == d.language_server_id)
74− let diagnostics_on_line = after.chain(before);
76+ });
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();
77+ diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
78−
79− write!(out, "●").unwrap();
78+ write!(out, "●").ok();
80− return Some(match diagnostic.severity {
79+ match d.severity {
81 Some(Severity::Error) => error,
80 Some(Severity::Error) => error,
82 Some(Severity::Warning) | None => warning,
81 Some(Severity::Warning) | None => warning,
83 Some(Severity::Info) => info,
82 Some(Severity::Info) => info,
84 Some(Severity::Hint) => hint,
83 Some(Severity::Hint) => hint,
85− });
84+ }
86− }
85+ })
87− None
88 },
86 },
89 )
87 )
90}
88}
languages.toml+235−180
@@ -1,6 +1,143 @@
1# Language support configuration.
1# Language support configuration.
2# See the languages documentation: https://docs.helix-editor.com/master/languages.html
2# See the languages documentation: https://docs.helix-editor.com/master/languages.html
3
3
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+
4[[language]]
141[[language]]
5name = "rust"
142name = "rust"
6scope = "source.rust"
143scope = "source.rust"
@@ -9,7 +146,7 @@ file-types = ["rs"]
9roots = ["Cargo.toml", "Cargo.lock"]
146roots = ["Cargo.toml", "Cargo.lock"]
10auto-format = true
147auto-format = true
11comment-token = "//"
148comment-token = "//"
12−language-server = { command = "rust-analyzer" }
149+language-servers = [ "rust-analyzer" ]
13indent = { tab-width = 4, unit = " " }
150indent = { tab-width = 4, unit = " " }
14
151
15[language.auto-pairs]
152[language.auto-pairs]
@@ -19,14 +156,6 @@ indent = { tab-width = 4, unit = " " }
19'"' = '"'
156'"' = '"'
20'`' = '`'
157'`' = '`'
21
158
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−
30[language.debugger]
159[language.debugger]
31name = "lldb-vscode"
160name = "lldb-vscode"
32transport = "stdio"
161transport = "stdio"
@@ -65,7 +194,7 @@ name = "sway"
65scope = "source.sway"
194scope = "source.sway"
66injection-regex = "sway"
195injection-regex = "sway"
67file-types = ["sw"]
196file-types = ["sw"]
68−language-server = { command = "forc", args = ["lsp"] }
197+language-servers = [ "forc" ]
69roots = ["Forc.toml", "Forc.lock"]
198roots = ["Forc.toml", "Forc.lock"]
70indent = { tab-width = 4, unit = " " }
199indent = { tab-width = 4, unit = " " }
71comment-token = "//"
200comment-token = "//"
@@ -81,7 +210,7 @@ injection-regex = "toml"
81file-types = ["toml", "poetry.lock"]
210file-types = ["toml", "poetry.lock"]
82roots = []
211roots = []
83comment-token = "#"
212comment-token = "#"
84−language-server = { command = "taplo", args = ["lsp", "stdio"] }
213+language-servers = [ "taplo" ]
85indent = { tab-width = 2, unit = " " }
214indent = { tab-width = 2, unit = " " }
86
215
87[[grammar]]
216[[grammar]]
@@ -95,7 +224,7 @@ injection-regex = "awk"
95file-types = ["awk", "gawk", "nawk", "mawk"]
224file-types = ["awk", "gawk", "nawk", "mawk"]
96roots = []
225roots = []
97comment-token = "#"
226comment-token = "#"
98−language-server = { command = "awk-language-server" }
227+language-servers = [ "awk-language-server" ]
99indent = { tab-width = 2, unit = " " }
228indent = { tab-width = 2, unit = " " }
100
229
101[[grammar]]
230[[grammar]]
@@ -123,8 +252,7 @@ file-types = ["ex", "exs", "mix.lock"]
123shebangs = ["elixir"]
252shebangs = ["elixir"]
124roots = ["mix.exs", "mix.lock"]
253roots = ["mix.exs", "mix.lock"]
125comment-token = "#"
254comment-token = "#"
126−language-server = { command = "elixir-ls" }
255+language-servers = [ "elixir-ls" ]
127−config = { elixirLS.dialyzerEnabled = false }
128indent = { tab-width = 2, unit = " " }
256indent = { tab-width = 2, unit = " " }
129
257
130[[grammar]]
258[[grammar]]
@@ -153,7 +281,7 @@ file-types = ["mint"]
153shebangs = []
281shebangs = []
154roots = []
282roots = []
155comment-token = "//"
283comment-token = "//"
156−language-server = { command = "mint", args = ["ls"] }
284+language-servers = [ "mint" ]
157indent = { tab-width = 2, unit = " " }
285indent = { tab-width = 2, unit = " " }
158
286
159[[language]]
287[[language]]
@@ -162,9 +290,8 @@ scope = "source.json"
162injection-regex = "json"
290injection-regex = "json"
163file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
291file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
164roots = []
292roots = []
165−language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
293+language-servers = [ "vscode-json-language-server" ]
166auto-format = true
294auto-format = true
167−config = { "provideFormatter" = true }
168indent = { tab-width = 2, unit = " " }
295indent = { tab-width = 2, unit = " " }
169
296
170[[grammar]]
297[[grammar]]
@@ -178,7 +305,7 @@ injection-regex = "c"
178file-types = ["c"] # TODO: ["h"]
305file-types = ["c"] # TODO: ["h"]
179roots = []
306roots = []
180comment-token = "//"
307comment-token = "//"
181−language-server = { command = "clangd" }
308+language-servers = [ "clangd" ]
182indent = { tab-width = 2, unit = " " }
309indent = { tab-width = 2, unit = " " }
183
310
184[language.debugger]
311[language.debugger]
@@ -215,7 +342,7 @@ injection-regex = "cpp"
215file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
342file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
216roots = []
343roots = []
217comment-token = "//"
344comment-token = "//"
218−language-server = { command = "clangd" }
345+language-servers = [ "clangd" ]
219indent = { tab-width = 2, unit = " " }
346indent = { tab-width = 2, unit = " " }
220
347
221[language.debugger]
348[language.debugger]
@@ -253,7 +380,7 @@ roots = ["shard.yml", "shard.lock"]
253comment-token = "#"
380comment-token = "#"
254indent = { tab-width = 2, unit = " " }
381indent = { tab-width = 2, unit = " " }
255grammar = "ruby"
382grammar = "ruby"
256−language-server = { command = "crystalline", args = ["--stdio"] }
383+language-servers = [ "crystalline" ]
257
384
258[[language]]
385[[language]]
259name = "c-sharp"
386name = "c-sharp"
@@ -263,7 +390,7 @@ file-types = ["cs"]
263roots = ["sln", "csproj"]
390roots = ["sln", "csproj"]
264comment-token = "//"
391comment-token = "//"
265indent = { tab-width = 4, unit = "\t" }
392indent = { tab-width = 4, unit = "\t" }
266−language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
393+language-servers = [ "omnisharp" ]
267
394
268[language.debugger]
395[language.debugger]
269name = "netcoredbg"
396name = "netcoredbg"
@@ -296,18 +423,10 @@ file-types = ["go"]
296roots = ["go.work", "go.mod"]
423roots = ["go.work", "go.mod"]
297auto-format = true
424auto-format = true
298comment-token = "//"
425comment-token = "//"
299−language-server = { command = "gopls" }
426+language-servers = [ "gopls" ]
300# TODO: gopls needs utf-8 offsets?
427# TODO: gopls needs utf-8 offsets?
301indent = { tab-width = 4, unit = "\t" }
428indent = { tab-width = 4, unit = "\t" }
302
429
303−[language.config.hints]
304−assignVariableTypes = true
305−compositeLiteralFields = true
306−constantValues = true
307−functionTypeParameters = true
308−parameterNames = true
309−rangeVariableTypes = true
310−
311[language.debugger]
430[language.debugger]
312name = "go"
431name = "go"
313transport = "tcp"
432transport = "tcp"
@@ -351,7 +470,7 @@ file-types = ["go.mod"]
351roots = []
470roots = []
352auto-format = true
471auto-format = true
353comment-token = "//"
472comment-token = "//"
354−language-server = { command = "gopls" }
473+language-servers = [ "gopls" ]
355indent = { tab-width = 4, unit = "\t" }
474indent = { tab-width = 4, unit = "\t" }
356
475
357[[grammar]]
476[[grammar]]
@@ -365,7 +484,7 @@ injection-regex = "gotmpl"
365file-types = ["gotmpl"]
484file-types = ["gotmpl"]
366roots = []
485roots = []
367comment-token = "//"
486comment-token = "//"
368−language-server = { command = "gopls" }
487+language-servers = [ "gopls" ]
369indent = { tab-width = 2, unit = " " }
488indent = { tab-width = 2, unit = " " }
370
489
371[[grammar]]
490[[grammar]]
@@ -380,7 +499,7 @@ file-types = ["go.work"]
380roots = []
499roots = []
381auto-format = true
500auto-format = true
382comment-token = "//"
501comment-token = "//"
383−language-server = { command = "gopls" }
502+language-servers = [ "gopls" ]
384indent = { tab-width = 4, unit = "\t" }
503indent = { tab-width = 4, unit = "\t" }
385
504
386[[grammar]]
505[[grammar]]
@@ -391,26 +510,15 @@ source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd
391name = "javascript"
510name = "javascript"
392scope = "source.js"
511scope = "source.js"
393injection-regex = "(js|javascript)"
512injection-regex = "(js|javascript)"
513+language-id = "javascript"
394file-types = ["js", "mjs", "cjs"]
514file-types = ["js", "mjs", "cjs"]
395shebangs = ["node"]
515shebangs = ["node"]
396roots = []
516roots = []
397comment-token = "//"
517comment-token = "//"
398# TODO: highlights-params
518# TODO: highlights-params
399−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
519+language-servers = [ "typescript-language-server" ]
400indent = { tab-width = 2, unit = " " }
520indent = { tab-width = 2, unit = " " }
401
521
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−
414[language.debugger]
522[language.debugger]
415name = "node-debug2"
523name = "node-debug2"
416transport = "stdio"
524transport = "stdio"
@@ -431,48 +539,26 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev =
431name = "jsx"
539name = "jsx"
432scope = "source.jsx"
540scope = "source.jsx"
433injection-regex = "jsx"
541injection-regex = "jsx"
542+language-id = "javascriptreact"
434file-types = ["jsx"]
543file-types = ["jsx"]
435roots = []
544roots = []
436comment-token = "//"
545comment-token = "//"
437−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascriptreact" }
546+language-servers = [ "typescript-language-server" ]
438indent = { tab-width = 2, unit = " " }
547indent = { tab-width = 2, unit = " " }
439grammar = "javascript"
548grammar = "javascript"
440
549
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−
453[[language]]
550[[language]]
454name = "typescript"
551name = "typescript"
455scope = "source.ts"
552scope = "source.ts"
456injection-regex = "(ts|typescript)"
553injection-regex = "(ts|typescript)"
457file-types = ["ts", "mts", "cts"]
554file-types = ["ts", "mts", "cts"]
555+language-id = "typescript"
458shebangs = []
556shebangs = []
459roots = []
557roots = []
460# TODO: highlights-params
558# TODO: highlights-params
461−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
559+language-servers = [ "typescript-language-server" ]
462indent = { tab-width = 2, unit = " " }
560indent = { tab-width = 2, unit = " " }
463
561
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−
476[[grammar]]
562[[grammar]]
477name = "typescript"
563name = "typescript"
478source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
564source = { 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 =
481name = "tsx"
567name = "tsx"
482scope = "source.tsx"
568scope = "source.tsx"
483injection-regex = "(tsx)" # |typescript
569injection-regex = "(tsx)" # |typescript
570+language-id = "typescriptreact"
484file-types = ["tsx"]
571file-types = ["tsx"]
485roots = []
572roots = []
486# TODO: highlights-params
573# TODO: highlights-params
487−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
574+language-servers = [ "typescript-language-server" ]
488indent = { tab-width = 2, unit = " " }
575indent = { tab-width = 2, unit = " " }
489
576
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−
502[[grammar]]
577[[grammar]]
503name = "tsx"
578name = "tsx"
504source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
579source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@@ -509,9 +584,8 @@ scope = "source.css"
509injection-regex = "css"
584injection-regex = "css"
510file-types = ["css", "scss"]
585file-types = ["css", "scss"]
511roots = []
586roots = []
512−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
587+language-servers = [ "vscode-css-language-server" ]
513auto-format = true
588auto-format = true
514−config = { "provideFormatter" = true }
515indent = { tab-width = 2, unit = " " }
589indent = { tab-width = 2, unit = " " }
516
590
517[[grammar]]
591[[grammar]]
@@ -524,9 +598,8 @@ scope = "source.scss"
524injection-regex = "scss"
598injection-regex = "scss"
525file-types = ["scss"]
599file-types = ["scss"]
526roots = []
600roots = []
527−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
601+language-servers = [ "vscode-css-language-server" ]
528auto-format = true
602auto-format = true
529−config = { "provideFormatter" = true }
530indent = { tab-width = 2, unit = " " }
603indent = { tab-width = 2, unit = " " }
531
604
532[[grammar]]
605[[grammar]]
@@ -539,9 +612,8 @@ scope = "text.html.basic"
539injection-regex = "html"
612injection-regex = "html"
540file-types = ["html"]
613file-types = ["html"]
541roots = []
614roots = []
542−language-server = { command = "vscode-html-language-server", args = ["--stdio"] }
615+language-servers = [ "vscode-html-language-server" ]
543auto-format = true
616auto-format = true
544−config = { "provideFormatter" = true }
545indent = { tab-width = 2, unit = " " }
617indent = { tab-width = 2, unit = " " }
546
618
547[[grammar]]
619[[grammar]]
@@ -556,7 +628,7 @@ file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstr
556shebangs = ["python"]
628shebangs = ["python"]
557roots = []
629roots = []
558comment-token = "#"
630comment-token = "#"
559−language-server = { command = "pylsp" }
631+language-servers = [ "pylsp" ]
560# TODO: pyls needs utf-8 offsets
632# TODO: pyls needs utf-8 offsets
561indent = { tab-width = 4, unit = " " }
633indent = { tab-width = 4, unit = " " }
562
634
@@ -572,7 +644,7 @@ file-types = ["ncl"]
572shebangs = []
644shebangs = []
573roots = []
645roots = []
574comment-token = "#"
646comment-token = "#"
575−language-server = { command = "nls" }
647+language-servers = [ "nls" ]
576indent = { tab-width = 2, unit = " " }
648indent = { tab-width = 2, unit = " " }
577
649
578[[grammar]]
650[[grammar]]
@@ -587,7 +659,7 @@ file-types = ["nix"]
587shebangs = []
659shebangs = []
588roots = []
660roots = []
589comment-token = "#"
661comment-token = "#"
590−language-server = { command = "nil" }
662+language-servers = [ "nil" ]
591indent = { tab-width = 2, unit = " " }
663indent = { tab-width = 2, unit = " " }
592
664
593[[grammar]]
665[[grammar]]
@@ -602,7 +674,7 @@ file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile",
602shebangs = ["ruby"]
674shebangs = ["ruby"]
603roots = []
675roots = []
604comment-token = "#"
676comment-token = "#"
605−language-server = { command = "solargraph", args = ["stdio"] }
677+language-servers = [ "solargraph" ]
606indent = { tab-width = 2, unit = " " }
678indent = { tab-width = 2, unit = " " }
607
679
608[[grammar]]
680[[grammar]]
@@ -617,7 +689,7 @@ file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile
617shebangs = ["sh", "bash", "dash", "zsh"]
689shebangs = ["sh", "bash", "dash", "zsh"]
618roots = []
690roots = []
619comment-token = "#"
691comment-token = "#"
620−language-server = { command = "bash-language-server", args = ["start"] }
692+language-servers = [ "bash-language-server" ]
621indent = { tab-width = 2, unit = " " }
693indent = { tab-width = 2, unit = " " }
622
694
623[[grammar]]
695[[grammar]]
@@ -631,7 +703,7 @@ injection-regex = "php"
631file-types = ["php", "inc"]
703file-types = ["php", "inc"]
632shebangs = ["php"]
704shebangs = ["php"]
633roots = ["composer.json", "index.php"]
705roots = ["composer.json", "index.php"]
634−language-server = { command = "intelephense", args = ["--stdio"] }
706+language-servers = [ "intelephense" ]
635indent = { tab-width = 4, unit = " " }
707indent = { tab-width = 4, unit = " " }
636
708
637[[grammar]]
709[[grammar]]
@@ -657,7 +729,7 @@ injection-regex = "tex"
657file-types = ["tex", "sty", "cls"]
729file-types = ["tex", "sty", "cls"]
658roots = []
730roots = []
659comment-token = "%"
731comment-token = "%"
660−language-server = { command = "texlab" }
732+language-servers = [ "texlab" ]
661indent = { tab-width = 4, unit = "\t" }
733indent = { tab-width = 4, unit = "\t" }
662
734
663[[grammar]]
735[[grammar]]
@@ -671,7 +743,7 @@ injection-regex = "bib"
671file-types = ["bib"]
743file-types = ["bib"]
672roots = []
744roots = []
673comment-token = "%"
745comment-token = "%"
674−language-server = { command = "texlab" }
746+language-servers = [ "texlab" ]
675indent = { tab-width = 4, unit = "\t" }
747indent = { tab-width = 4, unit = "\t" }
676auto-format = true
748auto-format = true
677
749
@@ -699,7 +771,7 @@ injection-regex = "lean"
699file-types = ["lean"]
771file-types = ["lean"]
700roots = [ "lakefile.lean" ]
772roots = [ "lakefile.lean" ]
701comment-token = "--"
773comment-token = "--"
702−language-server = { command = "lean", args = [ "--server" ] }
774+language-servers = [ "lean" ]
703indent = { tab-width = 2, unit = " " }
775indent = { tab-width = 2, unit = " " }
704
776
705[[grammar]]
777[[grammar]]
@@ -713,13 +785,7 @@ injection-regex = "julia"
713file-types = ["jl"]
785file-types = ["jl"]
714roots = ["Manifest.toml", "Project.toml"]
786roots = ["Manifest.toml", "Project.toml"]
715comment-token = "#"
787comment-token = "#"
716−language-server = { command = "julia", timeout = 60, args = [
788+language-servers = [ "julia" ]
717− "--startup-file=no",
718− "--history-file=no",
719− "--quiet",
720− "-e",
721− "using LanguageServer; runserver()",
722− ] }
723indent = { tab-width = 4, unit = " " }
789indent = { tab-width = 4, unit = " " }
724
790
725[[grammar]]
791[[grammar]]
@@ -732,7 +798,7 @@ scope = "source.java"
732injection-regex = "java"
798injection-regex = "java"
733file-types = ["java"]
799file-types = ["java"]
734roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
800roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
735−language-server = { command = "jdtls" }
801+language-servers = [ "jdtls" ]
736indent = { tab-width = 4, unit = " " }
802indent = { tab-width = 4, unit = " " }
737
803
738[[grammar]]
804[[grammar]]
@@ -773,7 +839,7 @@ file-types = ["ml"]
773shebangs = []
839shebangs = []
774roots = []
840roots = []
775comment-token = "(**)"
841comment-token = "(**)"
776−language-server = { command = "ocamllsp" }
842+language-servers = [ "ocamllsp" ]
777indent = { tab-width = 2, unit = " " }
843indent = { tab-width = 2, unit = " " }
778
844
779[language.auto-pairs]
845[language.auto-pairs]
@@ -794,7 +860,7 @@ file-types = ["mli"]
794shebangs = []
860shebangs = []
795roots = []
861roots = []
796comment-token = "(**)"
862comment-token = "(**)"
797−language-server = { command = "ocamllsp" }
863+language-servers = [ "ocamllsp" ]
798indent = { tab-width = 2, unit = " " }
864indent = { tab-width = 2, unit = " " }
799
865
800[language.auto-pairs]
866[language.auto-pairs]
@@ -817,15 +883,7 @@ shebangs = ["lua"]
817roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
883roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
818comment-token = "--"
884comment-token = "--"
819indent = { tab-width = 2, unit = " " }
885indent = { tab-width = 2, unit = " " }
820−language-server = { command = "lua-language-server", args = [] }
886+language-servers = [ "lua-language-server" ]
821−
822−[language.config.Lua.hint]
823−enable = true
824−arrayIndex = "Enable"
825−setType = true
826−paramName = "All"
827−paramType = true
828−await = true
829
887
830[[grammar]]
888[[grammar]]
831name = "lua"
889name = "lua"
@@ -838,7 +896,7 @@ injection-regex = "svelte"
838file-types = ["svelte"]
896file-types = ["svelte"]
839roots = []
897roots = []
840indent = { tab-width = 2, unit = " " }
898indent = { tab-width = 2, unit = " " }
841−language-server = { command = "svelteserver", args = ["--stdio"] }
899+language-servers = [ "svelteserver" ]
842
900
843[[grammar]]
901[[grammar]]
844name = "svelte"
902name = "svelte"
@@ -851,7 +909,7 @@ injection-regex = "vue"
851file-types = ["vue"]
909file-types = ["vue"]
852roots = ["package.json", "vue.config.js"]
910roots = ["package.json", "vue.config.js"]
853indent = { tab-width = 2, unit = " " }
911indent = { tab-width = 2, unit = " " }
854−language-server = { command = "vls" }
912+language-servers = [ "vuels" ]
855
913
856[[grammar]]
914[[grammar]]
857name = "vue"
915name = "vue"
@@ -864,7 +922,7 @@ file-types = ["yml", "yaml"]
864roots = []
922roots = []
865comment-token = "#"
923comment-token = "#"
866indent = { tab-width = 2, unit = " " }
924indent = { tab-width = 2, unit = " " }
867−language-server = { command = "yaml-language-server", args = ["--stdio"] }
925+language-servers = [ "yaml-language-server" ]
868injection-regex = "yml|yaml"
926injection-regex = "yml|yaml"
869
927
870[[grammar]]
928[[grammar]]
@@ -878,7 +936,7 @@ injection-regex = "haskell"
878file-types = ["hs", "hs-boot"]
936file-types = ["hs", "hs-boot"]
879roots = ["Setup.hs", "stack.yaml", "cabal.project"]
937roots = ["Setup.hs", "stack.yaml", "cabal.project"]
880comment-token = "--"
938comment-token = "--"
881−language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
939+language-servers = [ "haskell-language-server" ]
882indent = { tab-width = 2, unit = " " }
940indent = { tab-width = 2, unit = " " }
883
941
884[[grammar]]
942[[grammar]]
@@ -892,7 +950,7 @@ injection-regex = "purescript"
892file-types = ["purs"]
950file-types = ["purs"]
893roots = ["spago.dhall", "bower.json"]
951roots = ["spago.dhall", "bower.json"]
894comment-token = "--"
952comment-token = "--"
895−language-server = { command = "purescript-language-server", args = ["--stdio"] }
953+language-servers = [ "purescript-language-server" ]
896indent = { tab-width = 2, unit = " " }
954indent = { tab-width = 2, unit = " " }
897auto-format = true
955auto-format = true
898formatter = { command = "purs-tidy", args = ["format"] }
956formatter = { command = "purs-tidy", args = ["format"] }
@@ -906,7 +964,7 @@ file-types = ["zig"]
906roots = ["build.zig"]
964roots = ["build.zig"]
907auto-format = true
965auto-format = true
908comment-token = "//"
966comment-token = "//"
909−language-server = { command = "zls" }
967+language-servers = [ "zls" ]
910indent = { tab-width = 4, unit = " " }
968indent = { tab-width = 4, unit = " " }
911formatter = { command = "zig" , args = ["fmt", "--stdin"] }
969formatter = { command = "zig" , args = ["fmt", "--stdin"] }
912
970
@@ -944,10 +1002,7 @@ roots = []
944file-types = ["pl", "prolog"]
1002file-types = ["pl", "prolog"]
945shebangs = ["swipl"]
1003shebangs = ["swipl"]
946comment-token = "%"
1004comment-token = "%"
947−language-server = { command = "swipl", args = [
1005+language-servers = [ "swipl" ]
948− "-g", "use_module(library(lsp_server))",
949− "-g", "lsp_server:main",
950− "-t", "halt", "--", "stdio"] }
951
1006
952[[language]]
1007[[language]]
953name = "tsq"
1008name = "tsq"
@@ -969,7 +1024,7 @@ file-types = ["cmake", "CMakeLists.txt"]
969roots = []
1024roots = []
970comment-token = "#"
1025comment-token = "#"
971indent = { tab-width = 2, unit = " " }
1026indent = { tab-width = 2, unit = " " }
972−language-server = { command = "cmake-language-server" }
1027+language-servers = [ "cmake-language-server" ]
973injection-regex = "cmake"
1028injection-regex = "cmake"
974
1029
975[[grammar]]
1030[[grammar]]
@@ -1009,7 +1064,7 @@ file-types = ["pl", "pm", "t"]
1009shebangs = ["perl"]
1064shebangs = ["perl"]
1010roots = []
1065roots = []
1011comment-token = "#"
1066comment-token = "#"
1012−language-server = { command = "perlnavigator", args= ["--stdio"] }
1067+language-servers = [ "perlnavigator" ]
1013indent = { tab-width = 2, unit = " " }
1068indent = { tab-width = 2, unit = " " }
1014
1069
1015[[grammar]]
1070[[grammar]]
@@ -1023,7 +1078,7 @@ roots = []
1023file-types = ["rkt", "rktd", "rktl", "scrbl"]
1078file-types = ["rkt", "rktd", "rktl", "scrbl"]
1024shebangs = ["racket"]
1079shebangs = ["racket"]
1025comment-token = ";"
1080comment-token = ";"
1026−language-server = { command = "racket", args = ["-l", "racket-langserver"] }
1081+language-servers = [ "racket" ]
1027grammar = "scheme"
1082grammar = "scheme"
1028
1083
1029[[language]]
1084[[language]]
@@ -1034,7 +1089,7 @@ file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
1034shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1089shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1035comment-token = ";"
1090comment-token = ";"
1036indent = { tab-width = 2, unit = " " }
1091indent = { tab-width = 2, unit = " " }
1037−language-server = { command = "cl-lsp", args = [ "stdio" ] }
1092+language-servers = [ "cl-lsp" ]
1038grammar = "scheme"
1093grammar = "scheme"
1039
1094
1040[language.auto-pairs]
1095[language.auto-pairs]
@@ -1060,7 +1115,7 @@ scope = "source.wgsl"
1060file-types = ["wgsl"]
1115file-types = ["wgsl"]
1061roots = []
1116roots = []
1062comment-token = "//"
1117comment-token = "//"
1063−language-server = { command = "wgsl_analyzer" }
1118+language-servers = [ "wgsl_analyzer" ]
1064indent = { tab-width = 4, unit = " " }
1119indent = { tab-width = 4, unit = " " }
1065
1120
1066[[grammar]]
1121[[grammar]]
@@ -1123,7 +1178,7 @@ scope = "source.md"
1123injection-regex = "md|markdown"
1178injection-regex = "md|markdown"
1124file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1179file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1125roots = [".marksman.toml"]
1180roots = [".marksman.toml"]
1126−language-server = { command = "marksman", args=["server"] }
1181+language-servers = [ "marksman" ]
1127indent = { tab-width = 2, unit = " " }
1182indent = { tab-width = 2, unit = " " }
1128
1183
1129[[grammar]]
1184[[grammar]]
@@ -1149,7 +1204,7 @@ file-types = ["dart"]
1149roots = ["pubspec.yaml"]
1204roots = ["pubspec.yaml"]
1150auto-format = true
1205auto-format = true
1151comment-token = "//"
1206comment-token = "//"
1152−language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
1207+language-servers = [ "dart" ]
1153indent = { tab-width = 2, unit = " " }
1208indent = { tab-width = 2, unit = " " }
1154
1209
1155[[grammar]]
1210[[grammar]]
@@ -1163,8 +1218,7 @@ roots = ["build.sbt", "build.sc", "build.gradle", "build.gradle.kts", "pom.xml",
1163file-types = ["scala", "sbt", "sc"]
1218file-types = ["scala", "sbt", "sc"]
1164comment-token = "//"
1219comment-token = "//"
1165indent = { tab-width = 2, unit = " " }
1220indent = { tab-width = 2, unit = " " }
1166−language-server = { command = "metals" }
1221+language-servers = [ "metals" ]
1167−config = { "isHttpEnabled" = true }
1168
1222
1169[[grammar]]
1223[[grammar]]
1170name = "scala"
1224name = "scala"
@@ -1178,7 +1232,7 @@ roots = ["Dockerfile", "Containerfile"]
1178file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
1232file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
1179comment-token = "#"
1233comment-token = "#"
1180indent = { tab-width = 2, unit = " " }
1234indent = { tab-width = 2, unit = " " }
1181−language-server = { command = "docker-langserver", args = ["--stdio"] }
1235+language-servers = [ "docker-langserver" ]
1182
1236
1183[[grammar]]
1237[[grammar]]
1184name = "dockerfile"
1238name = "dockerfile"
@@ -1218,7 +1272,7 @@ roots = []
1218file-types = ["git-rebase-todo"]
1272file-types = ["git-rebase-todo"]
1219injection-regex = "git-rebase"
1273injection-regex = "git-rebase"
1220comment-token = "#"
1274comment-token = "#"
1221−indent = { tab-width = 2, unit = " " }
1275+indent = { tab-width = 2, unit = "y" }
1222
1276
1223[[grammar]]
1277[[grammar]]
1224name = "git-rebase"
1278name = "git-rebase"
@@ -1294,7 +1348,7 @@ file-types = ["elm"]
1294roots = ["elm.json"]
1348roots = ["elm.json"]
1295auto-format = true
1349auto-format = true
1296comment-token = "--"
1350comment-token = "--"
1297−language-server = { command = "elm-language-server" }
1351+language-servers = [ "elm-language-server" ]
1298indent = { tab-width = 4, unit = " " }
1352indent = { tab-width = 4, unit = " " }
1299
1353
1300[[grammar]]
1354[[grammar]]
@@ -1320,7 +1374,7 @@ file-types = ["res"]
1320roots = ["bsconfig.json"]
1374roots = ["bsconfig.json"]
1321auto-format = true
1375auto-format = true
1322comment-token = "//"
1376comment-token = "//"
1323−language-server = { command = "rescript-language-server", args = ["--stdio"] }
1377+language-servers = [ "rescript-language-server" ]
1324indent = { tab-width = 2, unit = " " }
1378indent = { tab-width = 2, unit = " " }
1325
1379
1326[[grammar]]
1380[[grammar]]
@@ -1336,7 +1390,7 @@ roots = ["rebar.config"]
1336shebangs = ["escript"]
1390shebangs = ["escript"]
1337comment-token = "%%"
1391comment-token = "%%"
1338indent = { tab-width = 4, unit = " " }
1392indent = { tab-width = 4, unit = " " }
1339−language-server = { command = "erlang_ls" }
1393+language-servers = [ "erlang-ls" ]
1340
1394
1341[language.auto-pairs]
1395[language.auto-pairs]
1342'(' = ')'
1396'(' = ')'
@@ -1357,7 +1411,7 @@ file-types = ["kt", "kts"]
1357roots = ["settings.gradle", "settings.gradle.kts"]
1411roots = ["settings.gradle", "settings.gradle.kts"]
1358comment-token = "//"
1412comment-token = "//"
1359indent = { tab-width = 4, unit = " " }
1413indent = { tab-width = 4, unit = " " }
1360−language-server = { command = "kotlin-language-server" }
1414+language-servers = [ "kotlin-language-server" ]
1361
1415
1362[[grammar]]
1416[[grammar]]
1363name = "kotlin"
1417name = "kotlin"
@@ -1367,11 +1421,12 @@ source = { git = "https://github.com/fwcd/tree-sitter-kotlin", rev = "a4f71eb9b8
1367name = "hcl"
1421name = "hcl"
1368scope = "source.hcl"
1422scope = "source.hcl"
1369injection-regex = "(hcl|tf|nomad)"
1423injection-regex = "(hcl|tf|nomad)"
1424+language-id = "terraform"
1370file-types = ["hcl", "tf", "nomad"]
1425file-types = ["hcl", "tf", "nomad"]
1371roots = []
1426roots = []
1372comment-token = "#"
1427comment-token = "#"
1373indent = { tab-width = 2, unit = " " }
1428indent = { tab-width = 2, unit = " " }
1374−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform" }
1429+language-servers = [ "terraform-ls" ]
1375auto-format = true
1430auto-format = true
1376
1431
1377[[grammar]]
1432[[grammar]]
@@ -1381,11 +1436,12 @@ source = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "3cb7
1381[[language]]
1436[[language]]
1382name = "tfvars"
1437name = "tfvars"
1383scope = "source.tfvars"
1438scope = "source.tfvars"
1439+language-id = "terraform-vars"
1384file-types = ["tfvars"]
1440file-types = ["tfvars"]
1385roots = []
1441roots = []
1386comment-token = "#"
1442comment-token = "#"
1387indent = { tab-width = 2, unit = " " }
1443indent = { tab-width = 2, unit = " " }
1388−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform-vars" }
1444+language-servers = [ "terraform-ls" ]
1389auto-format = true
1445auto-format = true
1390grammar = "hcl"
1446grammar = "hcl"
1391
1447
@@ -1409,7 +1465,7 @@ file-types = ["sol"]
1409roots = []
1465roots = []
1410comment-token = "//"
1466comment-token = "//"
1411indent = { tab-width = 4, unit = " " }
1467indent = { tab-width = 4, unit = " " }
1412−language-server = { command = "solc", args = ["--lsp"] }
1468+language-servers = [ "solc" ]
1413
1469
1414[[grammar]]
1470[[grammar]]
1415name = "solidity"
1471name = "solidity"
@@ -1423,7 +1479,7 @@ file-types = ["gleam"]
1423roots = ["gleam.toml"]
1479roots = ["gleam.toml"]
1424comment-token = "//"
1480comment-token = "//"
1425indent = { tab-width = 2, unit = " " }
1481indent = { tab-width = 2, unit = " " }
1426−language-server = { command = "gleam", args = ["lsp"] }
1482+language-servers = [ "gleam" ]
1427
1483
1428[[grammar]]
1484[[grammar]]
1429name = "gleam"
1485name = "gleam"
@@ -1447,7 +1503,7 @@ file-types = ["robot", "resource"]
1447comment-token = "#"
1503comment-token = "#"
1448roots = []
1504roots = []
1449indent = { tab-width = 4, unit = " " }
1505indent = { tab-width = 4, unit = " " }
1450−language-server = { command = "robotframework_ls" }
1506+language-servers = [ "robotframework_ls" ]
1451
1507
1452[[grammar]]
1508[[grammar]]
1453name = "robot"
1509name = "robot"
@@ -1462,7 +1518,7 @@ shebangs = ["r", "R"]
1462roots = []
1518roots = []
1463comment-token = "#"
1519comment-token = "#"
1464indent = { tab-width = 2, unit = " " }
1520indent = { tab-width = 2, unit = " " }
1465−language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
1521+language-servers = [ "r" ]
1466
1522
1467[[grammar]]
1523[[grammar]]
1468name = "r"
1524name = "r"
@@ -1476,7 +1532,7 @@ file-types = ["rmd", "Rmd"]
1476roots = []
1532roots = []
1477indent = { tab-width = 2, unit = " " }
1533indent = { tab-width = 2, unit = " " }
1478grammar = "markdown"
1534grammar = "markdown"
1479−language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
1535+language-servers = [ "r" ]
1480
1536
1481[[language]]
1537[[language]]
1482name = "swift"
1538name = "swift"
@@ -1486,7 +1542,7 @@ file-types = ["swift"]
1486roots = [ "Package.swift" ]
1542roots = [ "Package.swift" ]
1487comment-token = "//"
1543comment-token = "//"
1488auto-format = true
1544auto-format = true
1489−language-server = { command = "sourcekit-lsp" }
1545+language-servers = [ "sourcekit-lsp" ]
1490
1546
1491[[grammar]]
1547[[grammar]]
1492name = "swift"
1548name = "swift"
@@ -1533,8 +1589,7 @@ injection-regex = "heex"
1533file-types = ["heex"]
1589file-types = ["heex"]
1534roots = ["mix.exs", "mix.lock"]
1590roots = ["mix.exs", "mix.lock"]
1535indent = { tab-width = 2, unit = " " }
1591indent = { tab-width = 2, unit = " " }
1536−language-server = { command = "elixir-ls" }
1592+language-servers = [ "elixir-ls" ]
1537−config = { elixirLS.dialyzerEnabled = false }
1538
1593
1539[[grammar]]
1594[[grammar]]
1540name = "heex"
1595name = "heex"
@@ -1605,7 +1660,7 @@ file-types = ["vala", "vapi"]
1605roots = []
1660roots = []
1606comment-token = "//"
1661comment-token = "//"
1607indent = { tab-width = 2, unit = " " }
1662indent = { tab-width = 2, unit = " " }
1608−language-server = { command = "vala-language-server" }
1663+language-servers = [ "vala-language-server" ]
1609
1664
1610[[grammar]]
1665[[grammar]]
1611name = "vala"
1666name = "vala"
@@ -1670,7 +1725,7 @@ auto-format = false
1670scope = "source.odin"
1725scope = "source.odin"
1671file-types = ["odin"]
1726file-types = ["odin"]
1672roots = ["ols.json"]
1727roots = ["ols.json"]
1673−language-server = { command = "ols", args = [] }
1728+language-servers = [ "ols" ]
1674comment-token = "//"
1729comment-token = "//"
1675indent = { tab-width = 4, unit = "\t" }
1730indent = { tab-width = 4, unit = "\t" }
1676
1731
@@ -1721,7 +1776,7 @@ scope = "source.v"
1721file-types = ["v", "vv", "vsh"]
1776file-types = ["v", "vv", "vsh"]
1722shebangs = ["v run"]
1777shebangs = ["v run"]
1723roots = ["v.mod"]
1778roots = ["v.mod"]
1724−language-server = { command = "v", args = ["ls"] }
1779+language-servers = [ "vlang-language-server" ]
1725auto-format = true
1780auto-format = true
1726comment-token = "//"
1781comment-token = "//"
1727indent = { tab-width = 4, unit = "\t" }
1782indent = { tab-width = 4, unit = "\t" }
@@ -1736,7 +1791,7 @@ scope = "source.verilog"
1736file-types = ["v", "vh", "sv", "svh"]
1791file-types = ["v", "vh", "sv", "svh"]
1737roots = []
1792roots = []
1738comment-token = "//"
1793comment-token = "//"
1739−language-server = { command = "svlangserver", args = [] }
1794+language-servers = [ "svlangserver" ]
1740indent = { tab-width = 2, unit = " " }
1795indent = { tab-width = 2, unit = " " }
1741injection-regex = "verilog"
1796injection-regex = "verilog"
1742
1797
@@ -1775,7 +1830,7 @@ injection-regex = "openscad"
1775file-types = ["scad"]
1830file-types = ["scad"]
1776roots = []
1831roots = []
1777comment-token = "//"
1832comment-token = "//"
1778−language-server = { command = "openscad-lsp", args = ["--stdio"] }
1833+language-servers = [ "openscad-lsp" ]
1779indent = { tab-width = 2, unit = "\t" }
1834indent = { tab-width = 2, unit = "\t" }
1780
1835
1781[[grammar]]
1836[[grammar]]
@@ -1789,7 +1844,7 @@ injection-regex = "prisma"
1789file-types = ["prisma"]
1844file-types = ["prisma"]
1790roots = ["package.json"]
1845roots = ["package.json"]
1791comment-token = "//"
1846comment-token = "//"
1792−language-server = { command = "prisma-language-server", args = ["--stdio"] }
1847+language-servers = [ "prisma-language-server" ]
1793indent = { tab-width = 2, unit = " " }
1848indent = { tab-width = 2, unit = " " }
1794
1849
1795[[grammar]]
1850[[grammar]]
@@ -1803,7 +1858,7 @@ injection-regex = "(clojure|clj|edn|boot)"
1803file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
1858file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
1804roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
1859roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
1805comment-token = ";"
1860comment-token = ";"
1806−language-server = { command = "clojure-lsp" }
1861+language-servers = [ "clojure-lsp" ]
1807indent = { tab-width = 2, unit = " " }
1862indent = { tab-width = 2, unit = " " }
1808
1863
1809[[grammar]]
1864[[grammar]]
@@ -1827,7 +1882,7 @@ file-types = ["elv"]
1827roots = []
1882roots = []
1828comment-token = "#"
1883comment-token = "#"
1829indent = { tab-width = 2, unit = " " }
1884indent = { tab-width = 2, unit = " " }
1830−language-server = { command = "elvish", args = ["-lsp"] }
1885+language-servers = [ "elvish" ]
1831grammar = "elvish"
1886grammar = "elvish"
1832
1887
1833[[grammar]]
1888[[grammar]]
@@ -1843,7 +1898,7 @@ shebangs = []
1843roots = []
1898roots = []
1844comment-token = "--"
1899comment-token = "--"
1845indent = { tab-width = 2, unit = " " }
1900indent = { tab-width = 2, unit = " " }
1846−language-server = { command = "idris2-lsp" }
1901+language-servers = [ "idris2-lsp" ]
1847
1902
1848[[language]]
1903[[language]]
1849name = "fortran"
1904name = "fortran"
@@ -1853,7 +1908,7 @@ file-types = ["f", "for", "f90", "f95", "f03"]
1853roots = ["fpm.toml"]
1908roots = ["fpm.toml"]
1854comment-token = "!"
1909comment-token = "!"
1855indent = { tab-width = 4, unit = " "}
1910indent = { tab-width = 4, unit = " "}
1856−language-server = { command = "fortls", args = ["--lowercase_intrinsics"] }
1911+language-servers = [ "fortls" ]
1857
1912
1858[[grammar]]
1913[[grammar]]
1859name = "fortran"
1914name = "fortran"
@@ -1880,7 +1935,7 @@ file-types = ["dot"]
1880roots = []
1935roots = []
1881comment-token = "//"
1936comment-token = "//"
1882indent = { tab-width = 4, unit = " " }
1937indent = { tab-width = 4, unit = " " }
1883−language-server = { command = "dot-language-server", args = ["--stdio"] }
1938+language-servers = [ "dot-language-server" ]
1884
1939
1885[[grammar]]
1940[[grammar]]
1886name = "dot"
1941name = "dot"
@@ -1894,7 +1949,7 @@ file-types = ["cue"]
1894roots = ["cue.mod"]
1949roots = ["cue.mod"]
1895auto-format = true
1950auto-format = true
1896comment-token = "//"
1951comment-token = "//"
1897−language-server = { command = "cuelsp" }
1952+language-servers = [ "cuelsp" ]
1898indent = { tab-width = 4, unit = "\t" }
1953indent = { tab-width = 4, unit = "\t" }
1899formatter = { command = "cue", args = ["fmt", "-"] }
1954formatter = { command = "cue", args = ["fmt", "-"] }
1900
1955
@@ -1910,7 +1965,7 @@ file-types = ["slint"]
1910roots = []
1965roots = []
1911comment-token = "//"
1966comment-token = "//"
1912indent = { tab-width = 4, unit = " " }
1967indent = { tab-width = 4, unit = " " }
1913−language-server = { command = "slint-lsp", args = [] }
1968+language-servers = [ "slint-lsp" ]
1914
1969
1915[[grammar]]
1970[[grammar]]
1916name = "slint"
1971name = "slint"
@@ -1962,7 +2017,7 @@ file-types = ["pas", "pp", "inc", "lpr", "lfm"]
1962roots = []
2017roots = []
1963comment-token = "//"
2018comment-token = "//"
1964indent = { tab-width = 2, unit = " " }
2019indent = { tab-width = 2, unit = " " }
1965−language-server = { command = "pasls", args = [] }
2020+language-servers = [ "pasls" ]
1966
2021
1967[[grammar]]
2022[[grammar]]
1968name = "pascal"
2023name = "pascal"
@@ -1987,7 +2042,7 @@ file-types = ["libsonnet", "jsonnet"]
1987roots = ["jsonnetfile.json"]
2042roots = ["jsonnetfile.json"]
1988comment-token = "//"
2043comment-token = "//"
1989indent = { tab-width = 2, unit = " " }
2044indent = { tab-width = 2, unit = " " }
1990−language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
2045+language-servers = [ "jsonnet-language-server" ]
1991
2046
1992[[grammar]]
2047[[grammar]]
1993name = "jsonnet"
2048name = "jsonnet"
@@ -2013,7 +2068,7 @@ file-types = ["bass"]
2013roots = []
2068roots = []
2014comment-token = ";"
2069comment-token = ";"
2015indent = { tab-width = 2, unit = " " }
2070indent = { tab-width = 2, unit = " " }
2016−language-server = { command = "bass", args = ["--lsp"] }
2071+language-servers = [ "bass" ]
2017
2072
2018[[grammar]]
2073[[grammar]]
2019name = "bass"
2074name = "bass"
@@ -2049,7 +2104,7 @@ roots = []
2049comment-token = "//"
2104comment-token = "//"
2050injection-regex = "d"
2105injection-regex = "d"
2051indent = { tab-width = 4, unit = " "}
2106indent = { tab-width = 4, unit = " "}
2052−language-server = { command = "serve-d" }
2107+language-servers = [ "serve-d" ]
2053formatter = { command = "dfmt" }
2108formatter = { command = "dfmt" }
2054
2109
2055[[grammar]]
2110[[grammar]]
@@ -2173,7 +2228,7 @@ roots = []
2173auto-format = true
2228auto-format = true
2174comment-token = "//"
2229comment-token = "//"
2175indent = { tab-width = 2, unit = " "}
2230indent = { tab-width = 2, unit = " "}
2176−language-server = { command = "bicep-langserver" }
2231+language-servers = [ "bicep-langserver" ]
2177
2232
2178[[grammar]]
2233[[grammar]]
2179name = "bicep"
2234name = "bicep"
@@ -2184,7 +2239,7 @@ name = "qml"
2184scope = "source.qml"
2239scope = "source.qml"
2185file-types = ["qml"]
2240file-types = ["qml"]
2186roots = []
2241roots = []
2187−language-server = { command = "qmlls" }
2242+language-servers = [ "qmlls" ]
2188indent = { tab-width = 4, unit = " " }
2243indent = { tab-width = 4, unit = " " }
2189grammar = "qmljs"
2244grammar = "qmljs"
2190
2245
@@ -2239,7 +2294,7 @@ file-types = ["dhall"]
2239roots = []
2294roots = []
2240comment-token = "--"
2295comment-token = "--"
2241indent = { tab-width = 2, unit = " " }
2296indent = { tab-width = 2, unit = " " }
2242−language-server = { command = "dhall-lsp-server" }
2297+language-servers = [ "dhall-lsp-server" ]
2243formatter = { command = "dhall" , args = ["format"] }
2298formatter = { command = "dhall" , args = ["format"] }
2244
2299
2245[[grammar]]
2300[[grammar]]
@@ -2401,7 +2456,7 @@ file-types = ["smithy"]
2401roots = ["smithy-build.json"]
2456roots = ["smithy-build.json"]
2402comment-token = "//"
2457comment-token = "//"
2403indent = { tab-width = 4, unit = " " }
2458indent = { tab-width = 4, unit = " " }
2404−language-server = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
2459+language-servers = [ "cs" ]
2405
2460
2406[[grammar]]
2461[[grammar]]
2407name = "smithy"
2462name = "smithy"
@@ -2413,7 +2468,7 @@ scope = "source.vhdl"
2413file-types = ["vhd", "vhdl"]
2468file-types = ["vhd", "vhdl"]
2414roots = []
2469roots = []
2415comment-token = "--"
2470comment-token = "--"
2416−language-server = { command = "vhdl_ls", args = [] }
2471+language-servers = [ "vhdl_ls" ]
2417indent = { tab-width = 2, unit = " " }
2472indent = { tab-width = 2, unit = " " }
2418injection-regex = "vhdl"
2473injection-regex = "vhdl"
2419
2474
@@ -2429,7 +2484,7 @@ injection-regex = "rego"
2429file-types = ["rego"]
2484file-types = ["rego"]
2430auto-format = true
2485auto-format = true
2431comment-token = "#"
2486comment-token = "#"
2432−language-server = { command = "regols" }
2487+language-servers = [ "regols" ]
2433grammar = "rego"
2488grammar = "rego"
2434
2489
2435[[grammar]]
2490[[grammar]]
@@ -2445,7 +2500,7 @@ shebangs = []
2445roots = []
2500roots = []
2446comment-token = "#"
2501comment-token = "#"
2447indent = { tab-width = 2, unit = " " }
2502indent = { tab-width = 2, unit = " " }
2448−language-server = { command = "nimlangserver" }
2503+language-servers = [ "nimlangserver" ]
2449
2504
2450[language.auto-pairs]
2505[language.auto-pairs]
2451'(' = ')'
2506'(' = ')'
@@ -2485,7 +2540,7 @@ name = "markdoc"
2485scope = "text.markdoc"
2540scope = "text.markdoc"
2486roots = []
2541roots = []
2487file-types = ["mdoc"]
2542file-types = ["mdoc"]
2488−language-server = { command = "markdoc-ls", args = ["--stdio"] }
2543+language-servers = [ "markdoc-ls" ]
2489
2544
2490[[grammar]]
2545[[grammar]]
2491name = "markdoc"
2546name = "markdoc"
@@ -2498,7 +2553,7 @@ injection-regex = "(cl|opencl)"
2498file-types = ["cl"]
2553file-types = ["cl"]
2499roots = []
2554roots = []
2500comment-token = "//"
2555comment-token = "//"
2501−language-server = { command = "clangd" }
2556+language-servers = [ "clangd" ]
2502
2557
2503[[grammar]]
2558[[grammar]]
2504name = "opencl"
2559name = "opencl"
xtask/src/docgen.rs+6−5
@@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
96 );
96 );
97 }
97 }
98 row.push(
98 row.push(
99+ lc.language_servers
100+ .iter()
99− lc.language_server
101+ .filter_map(|ls| config.language_server.get(&ls.name))
100− .as_ref()
101− .map(|s| s.command.clone())
102+ .map(|s| md_mono(&s.command.clone()))
102− .map(|c| md_mono(&c))
103+ .collect::<Vec<_>>()
103− .unwrap_or_default(),
104+ .join(", "),
104 );
105 );
105
106
106 md.push_str(&md_table_row(&row));
107 md.push_str(&md_table_row(&row));