on master help sign in
Adds support for multiple language servers per language.

Language Servers are now configured in a separate table in `languages.toml`: ```toml [langauge-server.mylang-lsp] command = "mylang-lsp" args = ["--stdio"] config = { provideFormatter = true } [language-server.efm-lsp-prettier] command = "efm-langserver" [language-server.efm-lsp-prettier.config] documentFormatting = true languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] } ``` The language server for a language is configured like this (`typescript-language-server` is configured by default): ```toml [[language]] name = "typescript" language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ] ``` or equivalent: ```toml [[language]] name = "typescript" language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ] ``` Each requested LSP feature is priorized in the order of the `language-servers` array. For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`). If no `except-features` or `only-features` is given all features for the language server are enabled, as long as the language server supports these. If it doesn't the next language server which supports the feature is tried. The list of supported features are: - `format` - `goto-definition` - `goto-declaration` - `goto-type-definition` - `goto-reference` - `goto-implementation` - `signature-help` - `hover` - `document-highlight` - `completion` - `code-action` - `workspace-command` - `document-symbols` - `workspace-symbols` - `diagnostics` - `rename-symbol` - `inlay-hints` Another side-effect/difference that comes with this PR, is that only one language server instance is started if different languages use the same language server.

commit71551d395b4e47804df2d8ecea99e34dbbf16157 authorPhilipp Mildenberger <philipp@mildenberger.me> dateMon 23 May 2022, 18:10 (4 years ago) parent7f5940b
22 files changed+1555−1058
book/src/generated/typable-cmd.md+1−1 book/src/guides/adding_languages.md+1 book/src/languages.md+87−17 helix-core/src/diagnostic.rs+1 helix-core/src/syntax.rs+102−11 helix-lsp/src/client.rs+9−3 helix-lsp/src/lib.rs+111−93 helix-lsp/src/transport.rs+43−20 helix-term/src/application.rs+62−64 helix-term/src/commands.rs+154−119 helix-term/src/commands/lsp.rs+438−361 helix-term/src/commands/typed.rs+73−33 helix-term/src/health.rs+19−8 helix-term/src/ui/completion.rs+42−43 helix-term/src/ui/editor.rs+5−12 helix-term/src/ui/mod.rs+9−12 helix-term/src/ui/statusline.rs+6−6 helix-view/src/document.rs+104−41 helix-view/src/editor.rs+36−28 helix-view/src/gutter.rs+1−1 languages.toml+245−180 xtask/src/docgen.rs+6−5
book/src/generated/typable-cmd.md+1−1
@@ -50,7 +50,7 @@
5050| `:reload-all` | Discard changes and reload all documents from the source files. |
5151| `:update`, `:u` | Write changes only if the file has been modified. |
5252| `:lsp-workspace-command` | Open workspace command picker |
53−| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
53+| `:lsp-restart` | Restarts the language servers used by the currently opened file |
5454| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
5555| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
5656| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
book/src/guides/adding_languages.md+1
@@ -9,6 +9,7 @@ below.
99 necessary configuration for the new language. For more information on
1010 language configuration, refer to the
1111 [language configuration section](../languages.md) of the documentation.
12+ A new language server can be added by extending the `[language-server]` table in the same file.
12132. If you are adding a new language or updating an existing language server
1314 configuration, run the command `cargo xtask docgen` to update the
1415 [Language Support](../lang-support.md) documentation.
book/src/languages.md+87−17
@@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
1818```toml
1919# in <config_dir>/helix/languages.toml
2020
21+[language-server.mylang-lsp]
22+command = "mylang-lsp"
23+
2124[[language]]
2225name = "rust"
2326auto-format = false
@@ -41,8 +44,8 @@ injection-regex = "mylang"
4144file-types = ["mylang", "myl"]
4245comment-token = "#"
4346indent = { tab-width = 2, unit = " " }
44−language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
4547formatter = { command = "mylang-formatter" , args = ["--stdin"] }
48+language-servers = [ "mylang-lsp" ]
4649```
4750
4851These configuration keys are available:
@@ -50,6 +53,7 @@ These configuration keys are available:
5053| Key | Description |
5154| ---- | ----------- |
5255| `name` | The name of the language |
56+| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
5357| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
5458| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
5559| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@@ -59,7 +63,7 @@ These configuration keys are available:
5963| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
6064| `comment-token` | The token to use as a comment-token |
6165| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
62−| `language-server` | The Language Server to run. See the Language Server configuration section below. |
66+| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
6367| `config` | Language Server configuration |
6468| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
6569| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
@@ -92,31 +96,97 @@ with the following priorities:
9296 replaced at runtime with the appropriate path separator for the operating
9397 system, so this rule would match against `.git\config` files on Windows.
9498
95−### Language Server configuration
99+## Language Server configuration
100+
101+Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
102+
103+For example:
104+
105+```toml
106+[language-server.mylang-lsp]
107+command = "mylang-lsp"
108+args = ["--stdio"]
109+config = { provideFormatter = true }
110+environment = { "ENV1" = "value1", "ENV2" = "value2" }
111+
112+[language-server.efm-lsp-prettier]
113+command = "efm-langserver"
114+
115+[language-server.efm-lsp-prettier.config]
116+documentFormatting = true
117+languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
118+```
96119
97−The `language-server` field takes the following keys:
120+These are the available options for a language server.
98121
99−| Key | Description |
100−| --- | ----------- |
101−| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
102−| `args` | A list of arguments to pass to the language server binary |
103−| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
104−| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer |
105−| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
122+| Key | Description |
123+| ---- | ----------- |
124+| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
125+| `args` | A list of arguments to pass to the language server binary |
126+| `config` | LSP initialization options |
127+| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
128+| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
106129
107−The top-level `config` field is used to configure the LSP initialization options. A `format`
108−sub-table within `config` can be used to pass extra formatting options to
109−[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook).
130+A `format` sub-table within `config` can be used to pass extra formatting options to
131+[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
110132For example with typescript:
111133
112134```toml
113−[[language]]
114−name = "typescript"
115−auto-format = true
135+[language-server.typescript-language-server]
116136# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
117137config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
118138```
119139
140+### Configuring Language Servers for a language
141+
142+The `language-servers` attribute in a language tells helix which language servers are used for this language.
143+They have to be defined in the `[language-server]` table as described in the previous section.
144+Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
145+In case multiple language servers are specified in the `language-servers` attribute of a `language`,
146+it's often useful to only enable/disable certain language-server features for these language servers.
147+For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
148+so everything else should be handled by the `typescript-language-server` (which is configured by default)
149+The language configuration for typescript could look like this:
150+
151+```toml
152+[[language]]
153+name = "typescript"
154+language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
155+```
156+
157+or equivalent:
158+
159+```toml
160+[[language]]
161+name = "typescript"
162+language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
163+```
164+
165+Each requested LSP feature is priorized in the order of the `language-servers` array.
166+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`).
167+If no `except-features` or `only-features` is given all features for the language server are enabled.
168+If a language server itself doesn't support a feature the next language server array entry will be tried (and so on).
169+
170+The list of supported features are:
171+
172+- `format`
173+- `goto-definition`
174+- `goto-declaration`
175+- `goto-type-definition`
176+- `goto-reference`
177+- `goto-implementation`
178+- `signature-help`
179+- `hover`
180+- `document-highlight`
181+- `completion`
182+- `code-action`
183+- `workspace-command`
184+- `document-symbols`
185+- `workspace-symbols`
186+- `diagnostics`
187+- `rename-symbol`
188+- `inlay-hints`
189+
120190## Tree-sitter grammar configuration
121191
122192The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
helix-core/src/diagnostic.rs+1
@@ -43,6 +43,7 @@ pub struct Diagnostic {
4343 pub message: String,
4444 pub severity: Option<Severity>,
4545 pub code: Option<NumberOrString>,
46+ pub language_server_id: usize,
4647 pub tags: Vec<DiagnosticTag>,
4748 pub source: Option<String>,
4849 pub data: Option<serde_json::Value>,
helix-core/src/syntax.rs+102−11
@@ -17,7 +17,7 @@ use std::{
1717 borrow::Cow,
1818 cell::RefCell,
1919 collections::{HashMap, VecDeque},
20− fmt,
20+ fmt::{self, Display},
2121 hash::{Hash, Hasher},
2222 mem::{replace, transmute},
2323 path::{Path, PathBuf},
@@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
6060}
6161
6262#[derive(Debug, Serialize, Deserialize)]
63+#[serde(rename_all = "kebab-case")]
6364pub struct Configuration {
6465 pub language: Vec<LanguageConfiguration>,
66+ #[serde(default)]
67+ pub language_server: HashMap<String, LanguageServerConfiguration>,
6568}
6669
6770impl Default for Configuration {
@@ -75,7 +78,10 @@ impl Default for Configuration {
7578#[serde(rename_all = "kebab-case", deny_unknown_fields)]
7679pub struct LanguageConfiguration {
7780 #[serde(rename = "name")]
78− pub language_id: String, // c-sharp, rust
81+ pub language_id: String, // c-sharp, rust, tsx
82+ #[serde(rename = "language-id")]
83+ // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
84+ pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
7985 pub scope: String, // source.rust
8086 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
8187 #[serde(default)]
@@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
8591 pub text_width: Option<usize>,
8692 pub soft_wrap: Option<SoftWrap>,
8793
88− #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
89− pub config: Option<serde_json::Value>,
90−
9194 #[serde(default)]
9295 pub auto_format: bool,
9396
@@ -107,8 +110,8 @@ pub struct LanguageConfiguration {
107110 #[serde(skip)]
108111 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
109112 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
110− #[serde(skip_serializing_if = "Option::is_none")]
111− pub language_server: Option<LanguageServerConfiguration>,
113+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
114+ pub language_servers: Vec<LanguageServerFeatureConfiguration>,
112115 #[serde(skip_serializing_if = "Option::is_none")]
113116 pub indent: Option<IndentationConfiguration>,
114117
@@ -208,6 +211,68 @@ impl<'de> Deserialize<'de> for FileType {
208211 }
209212}
210213
214+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
215+#[serde(rename_all = "kebab-case")]
216+pub enum LanguageServerFeature {
217+ Format,
218+ GotoDeclaration,
219+ GotoDefinition,
220+ GotoTypeDefinition,
221+ GotoReference,
222+ GotoImplementation,
223+ // Goto, use bitflags, combining previous Goto members?
224+ SignatureHelp,
225+ Hover,
226+ DocumentHighlight,
227+ Completion,
228+ CodeAction,
229+ WorkspaceCommand,
230+ DocumentSymbols,
231+ WorkspaceSymbols,
232+ // Symbols, use bitflags, see above?
233+ Diagnostics,
234+ RenameSymbol,
235+ InlayHints,
236+}
237+
238+impl Display for LanguageServerFeature {
239+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240+ match self {
241+ LanguageServerFeature::Format => write!(f, "format"),
242+ LanguageServerFeature::GotoDeclaration => write!(f, "goto-declaration"),
243+ LanguageServerFeature::GotoDefinition => write!(f, "goto-definition"),
244+ LanguageServerFeature::GotoTypeDefinition => write!(f, "goto-type-definition"),
245+ LanguageServerFeature::GotoReference => write!(f, "goto-type-definition"),
246+ LanguageServerFeature::GotoImplementation => write!(f, "goto-implementation"),
247+ LanguageServerFeature::SignatureHelp => write!(f, "signature-help"),
248+ LanguageServerFeature::Hover => write!(f, "hover"),
249+ LanguageServerFeature::DocumentHighlight => write!(f, "document-highlight"),
250+ LanguageServerFeature::Completion => write!(f, "completion"),
251+ LanguageServerFeature::CodeAction => write!(f, "code-action"),
252+ LanguageServerFeature::WorkspaceCommand => write!(f, "workspace-command"),
253+ LanguageServerFeature::DocumentSymbols => write!(f, "document-symbols"),
254+ LanguageServerFeature::WorkspaceSymbols => write!(f, "workspace-symbols"),
255+ LanguageServerFeature::Diagnostics => write!(f, "diagnostics"),
256+ LanguageServerFeature::RenameSymbol => write!(f, "rename-symbol"),
257+ LanguageServerFeature::InlayHints => write!(f, "inlay-hints"),
258+ }
259+ }
260+}
261+
262+#[derive(Debug, Serialize, Deserialize)]
263+#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
264+pub enum LanguageServerFeatureConfiguration {
265+ #[serde(rename_all = "kebab-case")]
266+ Features {
267+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
268+ only_features: Vec<LanguageServerFeature>,
269+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
270+ except_features: Vec<LanguageServerFeature>,
271+ name: String,
272+ },
273+ Simple(String),
274+}
275+
211276#[derive(Debug, Serialize, Deserialize)]
212277#[serde(rename_all = "kebab-case")]
213278pub struct LanguageServerConfiguration {
@@ -217,9 +282,10 @@ pub struct LanguageServerConfiguration {
217282 pub args: Vec<String>,
218283 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
219284 pub environment: HashMap<String, String>,
285+ #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
286+ pub config: Option<serde_json::Value>,
220287 #[serde(default = "default_timeout")]
221288 pub timeout: u64,
222− pub language_id: Option<String>,
223289}
224290
225291#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -584,6 +650,15 @@ pub struct SoftWrap {
584650 pub wrap_at_text_width: Option<bool>,
585651}
586652
653+impl LanguageServerFeatureConfiguration {
654+ pub fn name(&self) -> &String {
655+ match self {
656+ LanguageServerFeatureConfiguration::Simple(name) => name,
657+ LanguageServerFeatureConfiguration::Features { name, .. } => name,
658+ }
659+ }
660+}
661+
587662// Expose loader as Lazy<> global since it's always static?
588663
589664#[derive(Debug)]
@@ -594,6 +669,8 @@ pub struct Loader {
594669 language_config_ids_by_suffix: HashMap<String, usize>,
595670 language_config_ids_by_shebang: HashMap<String, usize>,
596671
672+ language_server_configs: HashMap<String, LanguageServerConfiguration>,
673+
597674 scopes: ArcSwap<Vec<String>>,
598675}
599676
@@ -601,6 +678,7 @@ impl Loader {
601678 pub fn new(config: Configuration) -> Self {
602679 let mut loader = Self {
603680 language_configs: Vec::new(),
681+ language_server_configs: config.language_server,
604682 language_config_ids_by_extension: HashMap::new(),
605683 language_config_ids_by_suffix: HashMap::new(),
606684 language_config_ids_by_shebang: HashMap::new(),
@@ -725,6 +803,10 @@ impl Loader {
725803 self.language_configs.iter()
726804 }
727805
806+ pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
807+ &self.language_server_configs
808+ }
809+
728810 pub fn set_scopes(&self, scopes: Vec<String>) {
729811 self.scopes.store(Arc::new(scopes));
730812
@@ -2370,7 +2452,10 @@ mod test {
23702452 "#,
23712453 );
23722454
2373− let loader = Loader::new(Configuration { language: vec![] });
2455+ let loader = Loader::new(Configuration {
2456+ language: vec![],
2457+ language_server: HashMap::new(),
2458+ });
23742459 let language = get_language("rust").unwrap();
23752460
23762461 let query = Query::new(language, query_str).unwrap();
@@ -2429,7 +2514,10 @@ mod test {
24292514 .map(String::from)
24302515 .collect();
24312516
2432− let loader = Loader::new(Configuration { language: vec![] });
2517+ let loader = Loader::new(Configuration {
2518+ language: vec![],
2519+ language_server: HashMap::new(),
2520+ });
24332521
24342522 let language = get_language("rust").unwrap();
24352523 let config = HighlightConfiguration::new(
@@ -2532,7 +2620,10 @@ mod test {
25322620 ) {
25332621 let source = Rope::from_str(source);
25342622
2535− let loader = Loader::new(Configuration { language: vec![] });
2623+ let loader = Loader::new(Configuration {
2624+ language: vec![],
2625+ language_server: HashMap::new(),
2626+ });
25362627 let language = get_language(language_name).unwrap();
25372628
25382629 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
helix-lsp/src/client.rs+9−3
@@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
4444#[derive(Debug)]
4545pub struct Client {
4646 id: usize,
47+ name: String,
4748 _process: Child,
4849 server_tx: UnboundedSender<Payload>,
4950 request_counter: AtomicU64,
@@ -166,8 +167,7 @@ impl Client {
166167 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167168 }
168169
169− #[allow(clippy::type_complexity)]
170− #[allow(clippy::too_many_arguments)]
170+ #[allow(clippy::type_complexity, clippy::too_many_arguments)]
171171 pub fn start(
172172 cmd: &str,
173173 args: &[String],
@@ -176,6 +176,7 @@ impl Client {
176176 root_markers: &[String],
177177 manual_roots: &[PathBuf],
178178 id: usize,
179+ name: String,
179180 req_timeout: u64,
180181 doc_path: Option<&std::path::PathBuf>,
181182 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@@ -200,7 +201,7 @@ impl Client {
200201 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201202
202203 let (server_rx, server_tx, initialize_notify) =
203− Transport::start(reader, writer, stderr, id);
204+ Transport::start(reader, writer, stderr, id, name.clone());
204205 let (workspace, workspace_is_cwd) = find_workspace();
205206 let workspace = path::get_normalized_path(&workspace);
206207 let root = find_lsp_workspace(
@@ -225,6 +226,7 @@ impl Client {
225226
226227 let client = Self {
227228 id,
229+ name,
228230 _process: process,
229231 server_tx,
230232 request_counter: AtomicU64::new(0),
@@ -240,6 +242,10 @@ impl Client {
240242 Ok((client, server_rx, initialize_notify))
241243 }
242244
245+ pub fn name(&self) -> &String {
246+ &self.name
247+ }
248+
243249 pub fn id(&self) -> usize {
244250 self.id
245251 }
helix-lsp/src/lib.rs+111−93
@@ -17,19 +17,16 @@ use helix_core::{
1717use tokio::sync::mpsc::UnboundedReceiver;
1818
1919use std::{
20− collections::{hash_map::Entry, HashMap},
20+ collections::HashMap,
2121 path::{Path, PathBuf},
22− sync::{
23− atomic::{AtomicUsize, Ordering},
24− Arc,
25− },
22+ sync::Arc,
2623};
2724
2825use thiserror::Error;
2926use tokio_stream::wrappers::UnboundedReceiverStream;
3027
3128pub type Result<T> = core::result::Result<T, Error>;
32−type LanguageId = String;
29+type LanguageServerName = String;
3330
3431#[derive(Error, Debug)]
3532pub enum Error {
@@ -49,7 +46,7 @@ pub enum Error {
4946 Other(#[from] anyhow::Error),
5047}
5148
52−#[derive(Clone, Copy, Debug, Default)]
49+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
5350pub enum OffsetEncoding {
5451 /// UTF-8 code units aka bytes
5552 Utf8,
@@ -624,23 +621,18 @@ impl Notification {
624621
625622#[derive(Debug)]
626623pub struct Registry {
627− inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
628−
629− counter: AtomicUsize,
624+ inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
625+ syn_loader: Arc<helix_core::syntax::Loader>,
626+ counter: usize,
630627 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
631628}
632629
633−impl Default for Registry {
634− fn default() -> Self {
635− Self::new()
636− }
637−}
638−
639630impl Registry {
640− pub fn new() -> Self {
631+ pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
641632 Self {
642633 inner: HashMap::new(),
643− counter: AtomicUsize::new(0),
634+ syn_loader,
635+ counter: 0,
644636 incoming: SelectAll::new(),
645637 }
646638 }
@@ -649,15 +641,43 @@ impl Registry {
649641 self.inner
650642 .values()
651643 .flatten()
652− .find(|(client_id, _)| client_id == &id)
653− .map(|(_, client)| client.as_ref())
644+ .find(|client| client.id() == id)
645+ .map(|client| &**client)
654646 }
655647
656648 pub fn remove_by_id(&mut self, id: usize) {
657− self.inner.retain(|_, clients| {
658− clients.retain(|&(client_id, _)| client_id != id);
659− !clients.is_empty()
660− })
649+ self.inner.retain(|_, language_servers| {
650+ language_servers.retain(|ls| id != ls.id());
651+ !language_servers.is_empty()
652+ });
653+ }
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+ self.counter += 1;
669+ let id = self.counter;
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)
661681 }
662682
663683 pub fn restart(
@@ -666,48 +686,46 @@ impl Registry {
666686 doc_path: Option<&std::path::PathBuf>,
667687 root_dirs: &[PathBuf],
668688 enable_snippets: bool,
669− ) -> Result<Option<Arc<Client>>> {
670− let config = match &language_config.language_server {
671− Some(config) => config,
672− None => return Ok(None),
673− };
674−
675− let scope = language_config.scope.clone();
676−
677− match self.inner.entry(scope) {
678− Entry::Vacant(_) => Ok(None),
679− Entry::Occupied(mut entry) => {
680− // initialize a new client
681− let id = self.counter.fetch_add(1, Ordering::Relaxed);
682−
683− let NewClientResult(client, incoming) = start_client(
684− id,
685− language_config,
686− config,
687− doc_path,
688− root_dirs,
689− enable_snippets,
690− )?;
691− self.incoming.push(UnboundedReceiverStream::new(incoming));
689+ ) -> Result<Vec<Arc<Client>>> {
690+ language_config
691+ .language_servers
692+ .iter()
693+ .filter_map(|config| {
694+ let name = config.name().clone();
695+
696+ #[allow(clippy::map_entry)]
697+ if self.inner.contains_key(&name) {
698+ let client = match self.start_client(
699+ name.clone(),
700+ language_config,
701+ doc_path,
702+ root_dirs,
703+ enable_snippets,
704+ ) {
705+ Ok(client) => client,
706+ error => return Some(error),
707+ };
708+ let old_clients = self.inner.insert(name, vec![client.clone()]).unwrap();
692709
693− let old_clients = entry.insert(vec![(id, client.clone())]);
710+ // TODO what if there are different language servers for different workspaces,
711+ // I think the language servers will be stopped without being restarted, which is not intended
712+ for old_client in old_clients {
713+ tokio::spawn(async move {
714+ let _ = old_client.force_shutdown().await;
715+ });
716+ }
694717
695− for (_, old_client) in old_clients {
696− tokio::spawn(async move {
697− let _ = old_client.force_shutdown().await;
698− });
718+ Some(Ok(client))
719+ } else {
720+ None
699721 }
700−
701− Ok(Some(client))
702− }
703− }
722+ })
723+ .collect()
704724 }
705725
706− pub fn stop(&mut self, language_config: &LanguageConfiguration) {
707− let scope = language_config.scope.clone();
708−
709− if let Some(clients) = self.inner.remove(&scope) {
710− for (_, client) in clients {
726+ pub fn stop(&mut self, name: &str) {
727+ if let Some(clients) = self.inner.remove(name) {
728+ for client in clients {
711729 tokio::spawn(async move {
712730 let _ = client.force_shutdown().await;
713731 });
@@ -721,37 +739,35 @@ impl Registry {
721739 doc_path: Option<&std::path::PathBuf>,
722740 root_dirs: &[PathBuf],
723741 enable_snippets: bool,
724− ) -> Result<Option<Arc<Client>>> {
725− let config = match &language_config.language_server {
726− Some(config) => config,
727− None => return Ok(None),
728− };
729−
730− let clients = self.inner.entry(language_config.scope.clone()).or_default();
731− // check if we already have a client for this documents root that we can reuse
732− if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
733− client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
734− }) {
735− return Ok(Some(client.1.clone()));
736− }
737− // initialize a new client
738− let id = self.counter.fetch_add(1, Ordering::Relaxed);
739−
740− let NewClientResult(client, incoming) = start_client(
741− id,
742− language_config,
743− config,
744− doc_path,
745− root_dirs,
746− enable_snippets,
747− )?;
748− clients.push((id, client.clone()));
749− self.incoming.push(UnboundedReceiverStream::new(incoming));
750− Ok(Some(client))
742+ ) -> Result<Vec<Arc<Client>>> {
743+ language_config
744+ .language_servers
745+ .iter()
746+ .map(|features| {
747+ let name = features.name();
748+ if let Some(clients) = self.inner.get_mut(name) {
749+ if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, client)| {
750+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
751+ }) {
752+ return Ok(client.clone());
753+ }
754+ }
755+ let client = self.start_client(
756+ name.clone(),
757+ language_config,
758+ doc_path,
759+ root_dirs,
760+ enable_snippets,
761+ )?;
762+ let clients = self.inner.entry(features.name().clone()).or_default();
763+ clients.push(client.clone());
764+ Ok(client)
765+ })
766+ .collect()
751767 }
752768
753769 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
754− self.inner.values().flatten().map(|(_, client)| client)
770+ self.inner.values().flatten()
755771 }
756772}
757773
@@ -833,26 +849,28 @@ impl LspProgressMap {
833849 }
834850}
835851
836−struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
852+struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
837853
838854/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
839855/// it is only called when it makes sense.
840856fn start_client(
841857 id: usize,
858+ name: String,
842859 config: &LanguageConfiguration,
843860 ls_config: &LanguageServerConfiguration,
844861 doc_path: Option<&std::path::PathBuf>,
845862 root_dirs: &[PathBuf],
846863 enable_snippets: bool,
847−) -> Result<NewClientResult> {
864+) -> Result<NewClient> {
848865 let (client, incoming, initialize_notify) = Client::start(
849866 &ls_config.command,
850867 &ls_config.args,
851− config.config.clone(),
868+ ls_config.config.clone(),
852869 ls_config.environment.clone(),
853870 &config.roots,
854871 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
855872 id,
873+ name,
856874 ls_config.timeout,
857875 doc_path,
858876 )?;
@@ -886,7 +904,7 @@ fn start_client(
886904 initialize_notify.notify_one();
887905 });
888906
889− Ok(NewClientResult(client, incoming))
907+ Ok(NewClient(client, incoming))
890908}
891909
892910/// Find an LSP workspace of a file using the following mechanism:
helix-lsp/src/transport.rs+43−20
@@ -38,6 +38,7 @@ enum ServerMessage {
3838#[derive(Debug)]
3939pub struct Transport {
4040 id: usize,
41+ name: String,
4142 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
4243}
4344
@@ -47,6 +48,7 @@ impl Transport {
4748 server_stdin: BufWriter<ChildStdin>,
4849 server_stderr: BufReader<ChildStderr>,
4950 id: usize,
51+ name: String,
5052 ) -> (
5153 UnboundedReceiver<(usize, jsonrpc::Call)>,
5254 UnboundedSender<Payload>,
@@ -58,6 +60,7 @@ impl Transport {
5860
5961 let transport = Self {
6062 id,
63+ name,
6164 pending_requests: Mutex::new(HashMap::default()),
6265 };
6366
@@ -83,6 +86,7 @@ impl Transport {
8386 async fn recv_server_message(
8487 reader: &mut (impl AsyncBufRead + Unpin + Send),
8588 buffer: &mut String,
89+ language_server_name: &str,
8690 ) -> Result<ServerMessage> {
8791 let mut content_length = None;
8892 loop {
@@ -124,7 +128,7 @@ impl Transport {
124128 reader.read_exact(&mut content).await?;
125129 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
126130
127− info!("<- {}", msg);
131+ info!("{language_server_name} <- {msg}");
128132
129133 // try parsing as output (server response) or call (server request)
130134 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@@ -135,12 +139,13 @@ impl Transport {
135139 async fn recv_server_error(
136140 err: &mut (impl AsyncBufRead + Unpin + Send),
137141 buffer: &mut String,
142+ language_server_name: &str,
138143 ) -> Result<()> {
139144 buffer.truncate(0);
140145 if err.read_line(buffer).await? == 0 {
141146 return Err(Error::StreamClosed);
142147 };
143− error!("err <- {:?}", buffer);
148+ error!("{language_server_name} err <- {buffer:?}");
144149
145150 Ok(())
146151 }
@@ -162,15 +167,17 @@ impl Transport {
162167 Payload::Notification(value) => serde_json::to_string(&value)?,
163168 Payload::Response(error) => serde_json::to_string(&error)?,
164169 };
165− self.send_string_to_server(server_stdin, json).await
170+ self.send_string_to_server(server_stdin, json, &self.name)
171+ .await
166172 }
167173
168174 async fn send_string_to_server(
169175 &self,
170176 server_stdin: &mut BufWriter<ChildStdin>,
171177 request: String,
178+ language_server_name: &str,
172179 ) -> Result<()> {
173− info!("-> {}", request);
180+ info!("{language_server_name} -> {request}");
174181
175182 // send the headers
176183 server_stdin
@@ -189,9 +196,13 @@ impl Transport {
189196 &self,
190197 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
191198 msg: ServerMessage,
199+ language_server_name: &str,
192200 ) -> Result<()> {
193201 match msg {
194− ServerMessage::Output(output) => self.process_request_response(output).await?,
202+ ServerMessage::Output(output) => {
203+ self.process_request_response(output, language_server_name)
204+ .await?
205+ }
195206 ServerMessage::Call(call) => {
196207 client_tx
197208 .send((self.id, call))
@@ -202,14 +213,18 @@ impl Transport {
202213 Ok(())
203214 }
204215
205− async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
216+ async fn process_request_response(
217+ &self,
218+ output: jsonrpc::Output,
219+ language_server_name: &str,
220+ ) -> Result<()> {
206221 let (id, result) = match output {
207222 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
208− info!("<- {}", result);
223+ info!("{language_server_name} <- {}", result);
209224 (id, Ok(result))
210225 }
211226 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
212− error!("<- {}", error);
227+ error!("{language_server_name} <- {error}");
213228 (id, Err(error.into()))
214229 }
215230 };
@@ -240,12 +255,17 @@ impl Transport {
240255 ) {
241256 let mut recv_buffer = String::new();
242257 loop {
243− match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
258+ match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
259+ .await
260+ {
244261 Ok(msg) => {
245− match transport.process_server_message(&client_tx, msg).await {
262+ match transport
263+ .process_server_message(&client_tx, msg, &transport.name)
264+ .await
265+ {
246266 Ok(_) => {}
247267 Err(err) => {
248− error!("err: <- {:?}", err);
268+ error!("{} err: <- {err:?}", transport.name);
249269 break;
250270 }
251271 };
@@ -270,7 +290,7 @@ impl Transport {
270290 params: jsonrpc::Params::None,
271291 }));
272292 match transport
273− .process_server_message(&client_tx, notification)
293+ .process_server_message(&client_tx, notification, &transport.name)
274294 .await
275295 {
276296 Ok(_) => {}
@@ -281,20 +301,22 @@ impl Transport {
281301 break;
282302 }
283303 Err(err) => {
284− error!("err: <- {:?}", err);
304+ error!("{} err: <- {err:?}", transport.name);
285305 break;
286306 }
287307 }
288308 }
289309 }
290310
291− async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
311+ async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
292312 let mut recv_buffer = String::new();
293313 loop {
294− match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
314+ match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
315+ .await
316+ {
295317 Ok(_) => {}
296318 Err(err) => {
297− error!("err: <- {:?}", err);
319+ error!("{} err: <- {err:?}", transport.name);
298320 break;
299321 }
300322 }
@@ -348,10 +370,11 @@ impl Transport {
348370 method: lsp_types::notification::Initialized::METHOD.to_string(),
349371 params: jsonrpc::Params::None,
350372 }));
351− match transport.process_server_message(&client_tx, notification).await {
373+ let language_server_name = &transport.name;
374+ match transport.process_server_message(&client_tx, notification, language_server_name).await {
352375 Ok(_) => {}
353376 Err(err) => {
354− error!("err: <- {:?}", err);
377+ error!("{language_server_name} err: <- {err:?}");
355378 }
356379 }
357380
@@ -361,7 +384,7 @@ impl Transport {
361384 match transport.send_payload_to_server(&mut server_stdin, msg).await {
362385 Ok(_) => {}
363386 Err(err) => {
364− error!("err: <- {:?}", err);
387+ error!("{language_server_name} err: <- {err:?}");
365388 }
366389 }
367390 }
@@ -380,7 +403,7 @@ impl Transport {
380403 match transport.send_payload_to_server(&mut server_stdin, msg).await {
381404 Ok(_) => {}
382405 Err(err) => {
383− error!("err: <- {:?}", err);
406+ error!("{} err: <- {err:?}", transport.name);
384407 }
385408 }
386409 }
helix-term/src/application.rs+62−64
@@ -30,6 +30,7 @@ use crate::{
3030
3131use log::{debug, error, warn};
3232use std::{
33+ collections::btree_map::Entry,
3334 io::{stdin, stdout},
3435 path::Path,
3536 sync::Arc,
@@ -564,7 +565,7 @@ impl Application {
564565 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565566 let id = doc.id();
566567 doc.detect_language(loader);
567− let _ = self.editor.refresh_language_server(id);
568+ self.editor.refresh_language_servers(id);
568569 }
569570
570571 // TODO: fix being overwritten by lsp
@@ -662,6 +663,18 @@ impl Application {
662663 ) {
663664 use helix_lsp::{Call, MethodCall, Notification};
664665
666+ macro_rules! language_server {
667+ () => {
668+ match self.editor.language_servers.get_by_id(server_id) {
669+ Some(language_server) => language_server,
670+ None => {
671+ warn!("can't find language server with id `{}`", server_id);
672+ return;
673+ }
674+ }
675+ };
676+ }
677+
665678 match call {
666679 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
667680 let notification = match Notification::parse(&method, params) {
@@ -677,14 +690,7 @@ impl Application {
677690
678691 match notification {
679692 Notification::Initialized => {
680− let language_server =
681− match self.editor.language_servers.get_by_id(server_id) {
682− Some(language_server) => language_server,
683− None => {
684− warn!("can't find language server with id `{}`", server_id);
685− return;
686− }
687− };
693+ let language_server = language_server!();
688694
689695 // Trigger a workspace/didChangeConfiguration notification after initialization.
690696 // This might not be required by the spec but Neovim does this as well, so it's
@@ -694,7 +700,7 @@ impl Application {
694700 }
695701
696702 let docs = self.editor.documents().filter(|doc| {
697− doc.language_server().map(|server| server.id()) == Some(server_id)
703+ doc.language_servers().iter().any(|l| l.id() == server_id)
698704 });
699705
700706 // trigger textDocument/didOpen for docs that are already open
@@ -723,6 +729,7 @@ impl Application {
723729 return;
724730 }
725731 };
732+ let offset_encoding = language_server!().offset_encoding();
726733 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
727734 if let Some(version) = params.version {
728735 if version != doc.version() {
@@ -745,18 +752,11 @@ impl Application {
745752 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
746753 use lsp::DiagnosticSeverity;
747754
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−
755755 // TODO: convert inside server
756756 let start = if let Some(start) = lsp_pos_to_pos(
757757 text,
758758 diagnostic.range.start,
759− language_server.offset_encoding(),
759+ offset_encoding,
760760 ) {
761761 start
762762 } else {
@@ -764,11 +764,9 @@ impl Application {
764764 return None;
765765 };
766766
767− let end = if let Some(end) = lsp_pos_to_pos(
768− text,
769− diagnostic.range.end,
770− language_server.offset_encoding(),
771− ) {
767+ let end = if let Some(end) =
768+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
769+ {
772770 end
773771 } else {
774772 log::warn!("lsp position out of bounds - {:?}", diagnostic);
@@ -807,14 +805,19 @@ impl Application {
807805 None => None,
808806 };
809807
810− let tags = if let Some(ref tags) = diagnostic.tags {
811− let new_tags = tags.iter().filter_map(|tag| {
812− match *tag {
813− lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
814− lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
815− _ => None
816− }
817− }).collect();
808+ let tags = if let Some(tags) = &diagnostic.tags {
809+ let new_tags = tags
810+ .iter()
811+ .filter_map(|tag| match *tag {
812+ lsp::DiagnosticTag::DEPRECATED => {
813+ Some(DiagnosticTag::Deprecated)
814+ }
815+ lsp::DiagnosticTag::UNNECESSARY => {
816+ Some(DiagnosticTag::Unnecessary)
817+ }
818+ _ => None,
819+ })
820+ .collect();
818821
819822 new_tags
820823 } else {
@@ -830,11 +833,12 @@ impl Application {
830833 tags,
831834 source: diagnostic.source.clone(),
832835 data: diagnostic.data.clone(),
836+ language_server_id: server_id,
833837 })
834838 })
835839 .collect();
836840
837− doc.set_diagnostics(diagnostics);
841+ doc.replace_diagnostics(diagnostics, server_id);
838842 }
839843
840844 // Sort diagnostics first by severity and then by line numbers.
@@ -842,13 +846,26 @@ impl Application {
842846 params
843847 .diagnostics
844848 .sort_unstable_by_key(|d| (d.severity, d.range.start));
849+ let diagnostics = params
850+ .diagnostics
851+ .into_iter()
852+ .map(|d| (d, server_id, offset_encoding))
853+ .collect();
845854
846855 // Insert the original lsp::Diagnostics here because we may have no open document
847856 // for diagnosic message and so we can't calculate the exact position.
848857 // When using them later in the diagnostics picker, we calculate them on-demand.
849− self.editor
850− .diagnostics
851− .insert(params.uri, params.diagnostics);
858+ match self.editor.diagnostics.entry(params.uri) {
859+ Entry::Occupied(o) => {
860+ let current_diagnostics = o.into_mut();
861+ // there may entries of other language servers, which is why we can't overwrite the whole entry
862+ current_diagnostics.retain(|(_, lsp_id, _)| *lsp_id != server_id);
863+ current_diagnostics.extend(diagnostics);
864+ }
865+ Entry::Vacant(v) => {
866+ v.insert(diagnostics);
867+ }
868+ };
852869 }
853870 Notification::ShowMessage(params) => {
854871 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -950,10 +967,12 @@ impl Application {
950967 .editor
951968 .documents_mut()
952969 .filter_map(|doc| {
953− if doc.language_server().map(|server| server.id())
954− == Some(server_id)
970+ if doc
971+ .language_servers()
972+ .iter()
973+ .any(|server| server.id() == server_id)
955974 {
956− doc.set_diagnostics(Vec::new());
975+ doc.clear_diagnostics(server_id);
957976 doc.url()
958977 } else {
959978 None
@@ -1029,28 +1048,15 @@ impl Application {
10291048 }))
10301049 }
10311050 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))
1051+ Ok(json!(&*language_server!().workspace_folders().await))
10361052 }
10371053 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1054+ let language_server = language_server!();
10381055 let result: Vec<_> = params
10391056 .items
10401057 .iter()
1041− .map(|item| {
1042− let mut config = match &item.scope_uri {
1043− Some(scope) => {
1044− let path = scope.to_file_path().ok()?;
1045− let doc = self.editor.document_by_path(path)?;
1046− doc.language_config()?.config.as_ref()?
1047− }
1048− None => self
1049− .editor
1050− .language_servers
1051− .get_by_id(server_id)?
1052− .config()?,
1053− };
1058+ .filter_map(|item| {
1059+ let mut config = language_server.config()?;
10541060 if let Some(section) = item.section.as_ref() {
10551061 for part in section.split('.') {
10561062 config = config.get(part)?;
@@ -1074,15 +1080,7 @@ impl Application {
10741080 }
10751081 };
10761082
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));
1083+ tokio::spawn(language_server!().reply(id, reply));
10861084 }
10871085 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
10881086 }
helix-term/src/commands.rs+154−119
@@ -23,6 +23,7 @@ use helix_core::{
2323 regex::{self, Regex, RegexBuilder},
2424 search::{self, CharMatcher},
2525 selection, shellwords, surround,
26+ syntax::LanguageServerFeature,
2627 text_annotations::TextAnnotations,
2728 textobject,
2829 tree_sitter::Node,
@@ -54,13 +55,13 @@ use crate::{
5455 job::Callback,
5556 keymap::ReverseKeymap,
5657 ui::{
57− self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
58− Popup, Prompt, PromptEvent,
58+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
59+ FilePicker, Picker, Popup, Prompt, PromptEvent,
5960 },
6061};
6162
6263use crate::job::{self, Jobs};
63−use futures_util::StreamExt;
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
6465use std::{collections::HashMap, fmt, future::Future};
6566use std::{collections::HashSet, num::NonZeroUsize};
6667
@@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
30293030
30303031fn goto_first_diag(cx: &mut Context) {
30313032 let (view, doc) = current!(cx.editor);
3032− let selection = match doc.diagnostics().first() {
3033+ let selection = match doc.shown_diagnostics().next() {
30333034 Some(diag) => Selection::single(diag.range.start, diag.range.end),
30343035 None => return,
30353036 };
@@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
30383039
30393040fn goto_last_diag(cx: &mut Context) {
30403041 let (view, doc) = current!(cx.editor);
3041− let selection = match doc.diagnostics().last() {
3042+ let selection = match doc.shown_diagnostics().last() {
30423043 Some(diag) => Selection::single(diag.range.start, diag.range.end),
30433044 None => return,
30443045 };
@@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
30543055 .cursor(doc.text().slice(..));
30553056
30563057 let diag = doc
3057− .diagnostics()
3058− .iter()
3058+ .shown_diagnostics()
30593059 .find(|diag| diag.range.start > cursor_pos)
3060− .or_else(|| doc.diagnostics().first());
3060+ .or_else(|| doc.shown_diagnostics().next());
30613061
30623062 let selection = match diag {
30633063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
@@ -3075,11 +3075,12 @@ fn goto_prev_diag(cx: &mut Context) {
30753075 .cursor(doc.text().slice(..));
30763076
30773077 let diag = doc
3078− .diagnostics()
3079− .iter()
3078+ .shown_diagnostics()
3079+ .collect::<Vec<_>>()
3080+ .into_iter()
30803081 .rev()
30813082 .find(|diag| diag.range.start < cursor_pos)
3082− .or_else(|| doc.diagnostics().last());
3083+ .or_else(|| doc.shown_diagnostics().last());
30833084
30843085 let selection = match diag {
30853086 // NOTE: the selection is reversed because we're jumping to the
@@ -3234,60 +3235,72 @@ pub mod insert {
32343235 use helix_lsp::lsp;
32353236 // if ch matches completion char, trigger completion
32363237 let doc = doc_mut!(cx.editor);
3237− let language_server = match doc.language_server() {
3238− Some(language_server) => language_server,
3239− None => return,
3240− };
3238+ let trigger_completion = doc
3239+ .language_servers_with_feature(LanguageServerFeature::Completion)
3240+ .iter()
3241+ .any(|ls| {
3242+ let capabilities = ls.capabilities();
32413243
3242− let capabilities = language_server.capabilities();
3244+ // TODO: what if trigger is multiple chars long
3245+ matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions {
3246+ trigger_characters: Some(triggers),
3247+ ..
3248+ }) if triggers.iter().any(|trigger| trigger.contains(ch)))
3249+ });
32433250
3244− if let Some(lsp::CompletionOptions {
3245− trigger_characters: Some(triggers),
3246− ..
3247− }) = &capabilities.completion_provider
3248− {
3249− // TODO: what if trigger is multiple chars long
3250− if triggers.iter().any(|trigger| trigger.contains(ch)) {
3251− cx.editor.clear_idle_timer();
3252− super::completion(cx);
3253− }
3251+ if trigger_completion {
3252+ cx.editor.clear_idle_timer();
3253+ super::completion(cx);
32543254 }
32553255 }
32563256
32573257 fn signature_help(cx: &mut Context, ch: char) {
3258+ use futures_util::FutureExt;
32583259 use helix_lsp::lsp;
32593260 // if ch matches signature_help char, trigger
3260− let doc = doc_mut!(cx.editor);
3261− // The language_server!() macro is not used here since it will
3262− // print an "LSP not active for current buffer" message on
3263− // every keypress.
3264− let language_server = match doc.language_server() {
3265− Some(language_server) => language_server,
3266− None => return,
3267− };
3268−
3269− let capabilities = language_server.capabilities();
3261+ let (view, doc) = current!(cx.editor);
3262+ // lsp doesn't tell us when to close the signature help, so we request
3263+ // the help information again after common close triggers which should
3264+ // return None, which in turn closes the popup.
3265+ let close_triggers = &[')', ';', '.'];
3266+ // TODO support multiple language servers (not just the first that is found)
3267+ let future = doc
3268+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
3269+ .iter()
3270+ .find_map(|ls| {
3271+ let capabilities = ls.capabilities();
3272+
3273+ match capabilities {
3274+ lsp::ServerCapabilities {
3275+ signature_help_provider:
3276+ Some(lsp::SignatureHelpOptions {
3277+ trigger_characters: Some(triggers),
3278+ // TODO: retrigger_characters
3279+ ..
3280+ }),
3281+ ..
3282+ } if triggers.iter().any(|trigger| trigger.contains(ch))
3283+ || close_triggers.contains(&ch) =>
3284+ {
3285+ let pos = doc.position(view.id, ls.offset_encoding());
3286+ ls.text_document_signature_help(doc.identifier(), pos, None)
3287+ }
3288+ _ if close_triggers.contains(&ch) => ls.text_document_signature_help(
3289+ doc.identifier(),
3290+ doc.position(view.id, ls.offset_encoding()),
3291+ None,
3292+ ),
3293+ // TODO: what if trigger is multiple chars long
3294+ _ => None,
3295+ }
3296+ });
32703297
3271− if let lsp::ServerCapabilities {
3272− signature_help_provider:
3273− Some(lsp::SignatureHelpOptions {
3274− trigger_characters: Some(triggers),
3275− // TODO: retrigger_characters
3276− ..
3277− }),
3278− ..
3279− } = capabilities
3280− {
3281− // TODO: what if trigger is multiple chars long
3282− let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
3283− // lsp doesn't tell us when to close the signature help, so we request
3284− // the help information again after common close triggers which should
3285− // return None, which in turn closes the popup.
3286− let close_triggers = &[')', ';', '.'];
3287−
3288− if is_trigger || close_triggers.contains(&ch) {
3289− super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
3290− }
3298+ if let Some(future) = future {
3299+ super::signature_help_impl_with_future(
3300+ cx,
3301+ future.boxed(),
3302+ SignatureHelpInvoked::Automatic,
3303+ )
32913304 }
32923305 }
32933306
@@ -3301,7 +3314,7 @@ pub mod insert {
33013314 Some(transaction)
33023315 }
33033316
3304− use helix_core::auto_pairs;
3317+ use helix_core::{auto_pairs, syntax::LanguageServerFeature};
33053318
33063319 pub fn insert_char(cx: &mut Context, c: char) {
33073320 let (view, doc) = current_ref!(cx.editor);
@@ -4046,55 +4059,55 @@ fn format_selections(cx: &mut Context) {
40464059 use helix_lsp::{lsp, util::range_to_lsp_range};
40474060
40484061 let (view, doc) = current!(cx.editor);
4062+ let view_id = view.id;
40494063
40504064 // via lsp if available
40514065 // TODO: else via tree-sitter indentation calculations
40524066
4053− let language_server = match doc.language_server() {
4054− Some(language_server) => language_server,
4055− None => return,
4056− };
4057−
4058− let ranges: Vec<lsp::Range> = doc
4059− .selection(view.id)
4060− .iter()
4061− .map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
4062− .collect();
4063−
4064− if ranges.len() != 1 {
4067+ if doc.selection(view_id).len() != 1 {
40654068 cx.editor
40664069 .set_error("format_selections only supports a single selection for now");
40674070 return;
40684071 }
40694072
4070− // TODO: handle fails
4071− // TODO: concurrent map over all ranges
4072−
4073− let range = ranges[0];
4074−
4075− let request = match language_server.text_document_range_formatting(
4076− doc.identifier(),
4077− range,
4078− lsp::FormattingOptions::default(),
4079− None,
4080− ) {
4081− Some(future) => future,
4073+ let (future, offset_encoding) = match doc
4074+ .language_servers_with_feature(LanguageServerFeature::Format)
4075+ .iter()
4076+ .find_map(|language_server| {
4077+ let offset_encoding = language_server.offset_encoding();
4078+ let ranges: Vec<lsp::Range> = doc
4079+ .selection(view_id)
4080+ .iter()
4081+ .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
4082+ .collect();
4083+
4084+ // TODO: handle fails
4085+ // TODO: concurrent map over all ranges
4086+
4087+ let range = ranges[0];
4088+
4089+ let future = language_server.text_document_range_formatting(
4090+ doc.identifier(),
4091+ range,
4092+ lsp::FormattingOptions::default(),
4093+ None,
4094+ )?;
4095+ Some((future, offset_encoding))
4096+ }) {
4097+ Some(future_offset_encoding) => future_offset_encoding,
40824098 None => {
40834099 cx.editor
4084− .set_error("Language server does not support range formatting");
4100+ .set_error("No language server supports range formatting");
40854101 return;
40864102 }
40874103 };
40884104
4089− let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default();
4105+ let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
40904106
4091− let transaction = helix_lsp::util::generate_transaction_from_edits(
4092− doc.text(),
4093− edits,
4094− language_server.offset_encoding(),
4095− );
4107+ let transaction =
4108+ helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
40964109
4097− doc.apply(&transaction, view.id);
4110+ doc.apply(&transaction, view_id);
40984111}
40994112
41004113fn join_selections_impl(cx: &mut Context, select_space: bool) {
@@ -4231,21 +4244,45 @@ pub fn completion(cx: &mut Context) {
42314244 doc.savepoint(view)
42324245 };
42334246
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();
42404247 let text = savepoint.text.clone();
42414248 let cursor = savepoint.cursor();
42424249
4243− let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
4250+ let mut futures: FuturesUnordered<_> = doc
4251+ .language_servers_with_feature(LanguageServerFeature::Completion)
4252+ .iter()
4253+ // TODO this should probably already been filtered in something like "language_servers_with_feature"
4254+ .filter_map(|language_server| {
4255+ let language_server_id = language_server.id();
4256+ let offset_encoding = language_server.offset_encoding();
4257+ let pos = pos_to_lsp_pos(doc.text(), cursor, helix_lsp::OffsetEncoding::Utf8);
4258+ let completion_request = language_server.completion(doc.identifier(), pos, None)?;
4259+
4260+ Some(async move {
4261+ let json = completion_request.await?;
4262+ let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
4263+
4264+ let items = match response {
4265+ Some(lsp::CompletionResponse::Array(items)) => items,
4266+ // TODO: do something with is_incomplete
4267+ Some(lsp::CompletionResponse::List(lsp::CompletionList {
4268+ is_incomplete: _is_incomplete,
4269+ items,
4270+ })) => items,
4271+ None => Vec::new(),
4272+ }
4273+ .into_iter()
4274+ .map(|item| CompletionItem {
4275+ item,
4276+ language_server_id,
4277+ offset_encoding,
4278+ resolved: false,
4279+ })
4280+ .collect();
42444281
4245− let future = match language_server.completion(doc.identifier(), pos, None) {
4246− Some(future) => future,
4247− None => return,
4248− };
4282+ anyhow::Ok(items)
4283+ })
4284+ })
4285+ .collect();
42494286
42504287 // setup a channel that allows the request to be canceled
42514288 let (tx, rx) = oneshot::channel();
@@ -4254,12 +4291,20 @@ pub fn completion(cx: &mut Context) {
42544291 // and the associated request is automatically dropped
42554292 cx.editor.completion_request_handle = Some(tx);
42564293 let future = async move {
4294+ let items_future = async move {
4295+ let mut items = Vec::new();
4296+ // TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
4297+ while let Some(mut lsp_items) = futures.try_next().await? {
4298+ items.append(&mut lsp_items);
4299+ }
4300+ anyhow::Ok(items)
4301+ };
42574302 tokio::select! {
42584303 biased;
42594304 _ = rx => {
4260− Ok(serde_json::Value::Null)
4305+ Ok(Vec::new())
42614306 }
4262− res = future => {
4307+ res = items_future => {
42634308 res
42644309 }
42654310 }
@@ -4293,9 +4338,9 @@ pub fn completion(cx: &mut Context) {
42934338 },
42944339 ));
42954340
4296− cx.callback(
4297− future,
4298− move |editor, compositor, response: Option<lsp::CompletionResponse>| {
4341+ cx.jobs.callback(async move {
4342+ let items = future.await?;
4343+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
42994344 let (view, doc) = current_ref!(editor);
43004345 // check if the completion request is stale.
43014346 //
@@ -4306,16 +4351,6 @@ pub fn completion(cx: &mut Context) {
43064351 return;
43074352 }
43084353
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−
43194354 if items.is_empty() {
43204355 // editor.set_error("No completion available");
43214356 return;
@@ -4326,7 +4361,6 @@ pub fn completion(cx: &mut Context) {
43264361 editor,
43274362 savepoint,
43284363 items,
4329− offset_encoding,
43304364 start_offset,
43314365 trigger_offset,
43324366 size,
@@ -4340,8 +4374,9 @@ pub fn completion(cx: &mut Context) {
43404374 {
43414375 compositor.remove(SignatureHelp::ID);
43424376 }
4343− },
4344− );
4377+ };
4378+ Ok(Callback::EditorCompositor(Box::new(call)))
4379+ });
43454380}
43464381
43474382// comments
@@ -5141,7 +5176,7 @@ async fn shell_impl_async(
51415176 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
51425177 .await?;
51435178 }
5144− Ok::<_, anyhow::Error>(())
5179+ anyhow::Ok(())
51455180 });
51465181 let (output, _) = tokio::join! {
51475182 process.wait_with_output(),
helix-term/src/commands/lsp.rs+438−361
@@ -1,4 +1,4 @@
1−use futures_util::FutureExt;
1+use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
22use helix_lsp::{
33 block_on,
44 lsp::{
@@ -8,6 +8,8 @@ use helix_lsp::{
88 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
99 OffsetEncoding,
1010};
11+use serde_json::Value;
12+use tokio_stream::StreamExt;
1113use tui::{
1214 text::{Span, Spans},
1315 widgets::Row,
@@ -15,7 +17,9 @@ use tui::{
1517
1618use super::{align_view, push_jump, Align, Context, Editor, Open};
1719
18−use helix_core::{path, text_annotations::InlineAnnotation, Selection};
20+use helix_core::{
21+ path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
22+};
1923use helix_view::{
2024 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
2125 editor::Action,
@@ -25,6 +29,7 @@ use helix_view::{
2529
2630use crate::{
2731 compositor::{self, Compositor},
32+ job::Callback,
2833 ui::{
2934 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
3035 Popup, PromptEvent,
@@ -35,24 +40,6 @@ use std::{
3540 cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
3641};
3742
38−/// Gets the language server that is attached to a document, and
39−/// if it's not active displays a status message. Using this macro
40−/// in a context where the editor automatically queries the LSP
41−/// (instead of when the user explicitly does so via a keybind like
42−/// `gd`) will spam the "LSP inactive" status message confusingly.
43−#[macro_export]
44−macro_rules! language_server {
45− ($editor:expr, $doc:expr) => {
46− match $doc.language_server() {
47− Some(language_server) => language_server,
48− None => {
49− $editor.set_status("Language server not active for current buffer");
50− return;
51− }
52− }
53− };
54−}
55−
5643impl ui::menu::Item for lsp::Location {
5744 /// Current working directory.
5845 type Data = PathBuf;
@@ -87,20 +74,30 @@ impl ui::menu::Item for lsp::Location {
8774 }
8875}
8976
90−impl ui::menu::Item for lsp::SymbolInformation {
77+struct SymbolInformationItem {
78+ symbol: lsp::SymbolInformation,
79+ offset_encoding: OffsetEncoding,
80+}
81+
82+impl ui::menu::Item for SymbolInformationItem {
9183 /// Path to currently focussed document
9284 type Data = Option<lsp::Url>;
9385
9486 fn format(&self, current_doc_path: &Self::Data) -> Row {
95− if current_doc_path.as_ref() == Some(&self.location.uri) {
96− self.name.as_str().into()
87+ if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
88+ self.symbol.name.as_str().into()
9789 } else {
98− match self.location.uri.to_file_path() {
90+ match self.symbol.location.uri.to_file_path() {
9991 Ok(path) => {
10092 let get_relative_path = path::get_relative_path(path.as_path());
101− format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
93+ format!(
94+ "{} ({})",
95+ &self.symbol.name,
96+ get_relative_path.to_string_lossy()
97+ )
98+ .into()
10299 }
103− Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
100+ Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
104101 }
105102 }
106103 }
@@ -116,6 +113,7 @@ struct DiagnosticStyles {
116113struct PickerDiagnostic {
117114 url: lsp::Url,
118115 diag: lsp::Diagnostic,
116+ offset_encoding: OffsetEncoding,
119117}
120118
121119impl ui::menu::Item for PickerDiagnostic {
@@ -211,21 +209,19 @@ fn jump_to_location(
211209 align_view(doc, view, Align::Center);
212210}
213211
214−fn sym_picker(
215− symbols: Vec<lsp::SymbolInformation>,
216− current_path: Option<lsp::Url>,
217− offset_encoding: OffsetEncoding,
218−) -> FilePicker<lsp::SymbolInformation> {
212+type SymbolPicker = FilePicker<SymbolInformationItem>;
213+
214+fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
219215 // TODO: drop current_path comparison and instead use workspace: bool flag?
220216 FilePicker::new(
221217 symbols,
222218 current_path.clone(),
223− move |cx, symbol, action| {
219+ move |cx, item, action| {
224220 let (view, doc) = current!(cx.editor);
225221 push_jump(view, doc);
226222
227− if current_path.as_ref() != Some(&symbol.location.uri) {
228− let uri = &symbol.location.uri;
223+ if current_path.as_ref() != Some(&item.symbol.location.uri) {
224+ let uri = &item.symbol.location.uri;
229225 let path = match uri.to_file_path() {
230226 Ok(path) => path,
231227 Err(_) => {
@@ -245,7 +241,7 @@ fn sym_picker(
245241 let (view, doc) = current!(cx.editor);
246242
247243 if let Some(range) =
248− lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
244+ lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
249245 {
250246 // we flip the range so that the cursor sits on the start of the symbol
251247 // (for example start of the function).
@@ -253,7 +249,7 @@ fn sym_picker(
253249 align_view(doc, view, Align::Center);
254250 }
255251 },
256− move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
252+ move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
257253 )
258254 .truncate_start(false)
259255}
@@ -266,10 +262,9 @@ enum DiagnosticsFormat {
266262
267263fn diag_picker(
268264 cx: &Context,
269− diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
265+ diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>,
270266 current_path: Option<lsp::Url>,
271267 format: DiagnosticsFormat,
272− offset_encoding: OffsetEncoding,
273268) -> FilePicker<PickerDiagnostic> {
274269 // TODO: drop current_path comparison and instead use workspace: bool flag?
275270
@@ -277,10 +272,11 @@ fn diag_picker(
277272 let mut flat_diag = Vec::new();
278273 for (url, diags) in diagnostics {
279274 flat_diag.reserve(diags.len());
280− for diag in diags {
275+ for (diag, _, offset_encoding) in diags {
281276 flat_diag.push(PickerDiagnostic {
282277 url: url.clone(),
283278 diag,
279+ offset_encoding,
284280 });
285281 }
286282 }
@@ -295,7 +291,13 @@ fn diag_picker(
295291 FilePicker::new(
296292 flat_diag,
297293 (styles, format),
298− move |cx, PickerDiagnostic { url, diag }, action| {
294+ move |cx,
295+ PickerDiagnostic {
296+ url,
297+ diag,
298+ offset_encoding,
299+ },
300+ action| {
299301 if current_path.as_ref() == Some(url) {
300302 let (view, doc) = current!(cx.editor);
301303 push_jump(view, doc);
@@ -306,14 +308,14 @@ fn diag_picker(
306308
307309 let (view, doc) = current!(cx.editor);
308310
309− if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
311+ if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
310312 // we flip the range so that the cursor sits on the start of the symbol
311313 // (for example start of the function).
312314 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
313315 align_view(doc, view, Align::Center);
314316 }
315317 },
316− move |_editor, PickerDiagnostic { url, diag }| {
318+ move |_editor, PickerDiagnostic { url, diag, .. }| {
317319 let location = lsp::Location::new(url.clone(), diag.range);
318320 Some(location_to_file_location(&location))
319321 },
@@ -323,126 +325,149 @@ fn diag_picker(
323325
324326pub fn symbol_picker(cx: &mut Context) {
325327 fn nested_to_flat(
326− list: &mut Vec<lsp::SymbolInformation>,
328+ list: &mut Vec<SymbolInformationItem>,
327329 file: &lsp::TextDocumentIdentifier,
328330 symbol: lsp::DocumentSymbol,
331+ offset_encoding: OffsetEncoding,
329332 ) {
330333 #[allow(deprecated)]
331− list.push(lsp::SymbolInformation {
332− name: symbol.name,
333− kind: symbol.kind,
334− tags: symbol.tags,
335− deprecated: symbol.deprecated,
336− location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
337− container_name: None,
334+ list.push(SymbolInformationItem {
335+ symbol: lsp::SymbolInformation {
336+ name: symbol.name,
337+ kind: symbol.kind,
338+ tags: symbol.tags,
339+ deprecated: symbol.deprecated,
340+ location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
341+ container_name: None,
342+ },
343+ offset_encoding,
338344 });
339345 for child in symbol.children.into_iter().flatten() {
340− nested_to_flat(list, file, child);
346+ nested_to_flat(list, file, child, offset_encoding);
341347 }
342348 }
343349 let doc = doc!(cx.editor);
344350
345− let language_server = language_server!(cx.editor, doc);
351+ let mut futures: FuturesUnordered<_> = doc
352+ .language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
353+ .iter()
354+ .filter_map(|ls| {
355+ let request = ls.document_symbols(doc.identifier())?;
356+ Some((request, ls.offset_encoding(), doc.identifier()))
357+ })
358+ .map(|(request, offset_encoding, doc_id)| async move {
359+ let json = request.await?;
360+ let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
361+ let symbols = match response {
362+ Some(symbols) => symbols,
363+ None => return anyhow::Ok(vec![]),
364+ };
365+ // lsp has two ways to represent symbols (flat/nested)
366+ // convert the nested variant to flat, so that we have a homogeneous list
367+ let symbols = match symbols {
368+ lsp::DocumentSymbolResponse::Flat(symbols) => symbols
369+ .into_iter()
370+ .map(|symbol| SymbolInformationItem {
371+ symbol,
372+ offset_encoding,
373+ })
374+ .collect(),
375+ lsp::DocumentSymbolResponse::Nested(symbols) => {
376+ let mut flat_symbols = Vec::new();
377+ for symbol in symbols {
378+ nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
379+ }
380+ flat_symbols
381+ }
382+ };
383+ Ok(symbols)
384+ })
385+ .collect();
346386 let current_url = doc.url();
347− let offset_encoding = language_server.offset_encoding();
348387
349− let future = match language_server.document_symbols(doc.identifier()) {
350− Some(future) => future,
351− None => {
352− cx.editor
353− .set_error("Language server does not support document symbols");
354− return;
355− }
356− };
357−
358− cx.callback(
359− future,
360− move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| {
361− if let Some(symbols) = response {
362− // lsp has two ways to represent symbols (flat/nested)
363− // convert the nested variant to flat, so that we have a homogeneous list
364− let symbols = match symbols {
365− lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
366− lsp::DocumentSymbolResponse::Nested(symbols) => {
367− let doc = doc!(editor);
368− let mut flat_symbols = Vec::new();
369− for symbol in symbols {
370− nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
371− }
372− flat_symbols
373− }
374− };
388+ if futures.is_empty() {
389+ cx.editor
390+ .set_error("No Language server does support document symbols");
391+ return;
392+ }
375393
376− let picker = sym_picker(symbols, current_url, offset_encoding);
377− compositor.push(Box::new(overlaid(picker)))
394+ cx.jobs.callback(async move {
395+ let mut symbols = Vec::new();
396+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
397+ while let Some(mut lsp_items) = futures.try_next().await? {
398+ symbols.append(&mut lsp_items);
399+ }
400+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
401+ if symbols.is_empty() {
402+ editor.set_error("No symbols available");
403+ return;
378404 }
379− },
380− )
405+ let picker = sym_picker(symbols, current_url);
406+ compositor.push(Box::new(overlaid(picker)))
407+ };
408+
409+ Ok(Callback::EditorCompositor(Box::new(call)))
410+ });
381411}
382412
383413pub fn workspace_symbol_picker(cx: &mut Context) {
384414 let doc = doc!(cx.editor);
385− let current_url = doc.url();
386− let language_server = language_server!(cx.editor, doc);
387− let offset_encoding = language_server.offset_encoding();
388− let future = match language_server.workspace_symbols("".to_string()) {
389− Some(future) => future,
390− None => {
391− cx.editor
392− .set_error("Language server does not support workspace symbols");
393− return;
415+
416+ let get_symbols = move |pattern: String, editor: &mut Editor| {
417+ let doc = doc!(editor);
418+ let mut futures: FuturesUnordered<_> = doc
419+ .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
420+ .iter()
421+ .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding())))
422+ .map(|(request, offset_encoding)| async move {
423+ let json = request.await?;
424+
425+ let response = serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
426+ .unwrap_or_default()
427+ .into_iter()
428+ .map(|symbol| SymbolInformationItem {
429+ symbol,
430+ offset_encoding,
431+ })
432+ .collect();
433+
434+ anyhow::Ok(response)
435+ })
436+ .collect();
437+
438+ if futures.is_empty() {
439+ editor.set_error("No Language server does support workspace symbols");
394440 }
395− };
396441
397− cx.callback(
398− future,
399− move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
400− let symbols = response.unwrap_or_default();
401− let picker = sym_picker(symbols, current_url, offset_encoding);
402− let get_symbols = |query: String, editor: &mut Editor| {
403− let doc = doc!(editor);
404− let language_server = match doc.language_server() {
405− Some(s) => s,
406− None => {
407− // This should not generally happen since the picker will not
408− // even open in the first place if there is no server.
409− return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
410− }
411− };
412− let symbol_request = match language_server.workspace_symbols(query) {
413− Some(future) => future,
414− None => {
415− // This should also not happen since the language server must have
416− // supported workspace symbols before to reach this block.
417− return async move {
418− Err(anyhow::anyhow!(
419− "Language server does not support workspace symbols"
420− ))
421− }
422− .boxed();
423− }
424− };
442+ async move {
443+ let mut symbols = Vec::new();
444+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
445+ while let Some(mut lsp_items) = futures.try_next().await? {
446+ symbols.append(&mut lsp_items);
447+ }
448+ anyhow::Ok(symbols)
449+ }
450+ .boxed()
451+ };
425452
426− let future = async move {
427− let json = symbol_request.await?;
428− let response: Option<Vec<lsp::SymbolInformation>> =
429− serde_json::from_value(json)?;
453+ let current_url = doc.url();
454+ let initial_symbols = get_symbols("".to_owned(), cx.editor);
430455
431− Ok(response.unwrap_or_default())
432− };
433− future.boxed()
434− };
456+ cx.jobs.callback(async move {
457+ let symbols = initial_symbols.await?;
458+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
459+ let picker = sym_picker(symbols, current_url);
435460 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
436461 compositor.push(Box::new(overlaid(dyn_picker)))
437− },
438− )
462+ };
463+
464+ Ok(Callback::EditorCompositor(Box::new(call)))
465+ });
439466}
440467
441468pub fn diagnostics_picker(cx: &mut Context) {
442469 let doc = doc!(cx.editor);
443− let language_server = language_server!(cx.editor, doc);
444470 if let Some(current_url) = doc.url() {
445− let offset_encoding = language_server.offset_encoding();
446471 let diagnostics = cx
447472 .editor
448473 .diagnostics
@@ -454,7 +479,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
454479 [(current_url.clone(), diagnostics)].into(),
455480 Some(current_url),
456481 DiagnosticsFormat::HideSourcePath,
457− offset_encoding,
458482 );
459483 cx.push_layer(Box::new(overlaid(picker)));
460484 }
@@ -462,24 +486,28 @@ pub fn diagnostics_picker(cx: &mut Context) {
462486
463487pub fn workspace_diagnostics_picker(cx: &mut Context) {
464488 let doc = doc!(cx.editor);
465− let language_server = language_server!(cx.editor, doc);
466489 let current_url = doc.url();
467− let offset_encoding = language_server.offset_encoding();
490+ // TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
468491 let diagnostics = cx.editor.diagnostics.clone();
469492 let picker = diag_picker(
470493 cx,
471494 diagnostics,
472495 current_url,
473496 DiagnosticsFormat::ShowSourcePath,
474− offset_encoding,
475497 );
476498 cx.push_layer(Box::new(overlaid(picker)));
477499}
478500
479−impl ui::menu::Item for lsp::CodeActionOrCommand {
501+struct CodeActionOrCommandItem {
502+ lsp_item: lsp::CodeActionOrCommand,
503+ offset_encoding: OffsetEncoding,
504+ language_server_id: usize,
505+}
506+
507+impl ui::menu::Item for CodeActionOrCommandItem {
480508 type Data = ();
481509 fn format(&self, _data: &Self::Data) -> Row {
482− match self {
510+ match &self.lsp_item {
483511 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
484512 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
485513 }
@@ -546,45 +574,40 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
546574pub fn code_action(cx: &mut Context) {
547575 let (view, doc) = current!(cx.editor);
548576
549− let language_server = language_server!(cx.editor, doc);
550−
551577 let selection_range = doc.selection(view.id).primary();
552− let offset_encoding = language_server.offset_encoding();
553−
554− let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
555578
556− let future = match language_server.code_actions(
557− doc.identifier(),
558− range,
559− // Filter and convert overlapping diagnostics
560− lsp::CodeActionContext {
561− diagnostics: doc
562− .diagnostics()
563− .iter()
564− .filter(|&diag| {
565− selection_range
566− .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
567− })
568− .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
569− .collect(),
570− only: None,
571− trigger_kind: Some(CodeActionTriggerKind::INVOKED),
572− },
573− ) {
574− Some(future) => future,
575− None => {
576− cx.editor
577− .set_error("Language server does not support code actions");
578− return;
579− }
580− };
581−
582− cx.callback(
583− future,
584− move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
579+ let mut futures: FuturesUnordered<_> = doc
580+ .language_servers_with_feature(LanguageServerFeature::CodeAction)
581+ .iter()
582+ // TODO this should probably already been filtered in something like "language_servers_with_feature"
583+ .filter_map(|language_server| {
584+ let offset_encoding = language_server.offset_encoding();
585+ let language_server_id = language_server.id();
586+ let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
587+ // Filter and convert overlapping diagnostics
588+ let code_action_context = lsp::CodeActionContext {
589+ diagnostics: doc
590+ .diagnostics()
591+ .iter()
592+ .filter(|&diag| {
593+ selection_range
594+ .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
595+ })
596+ .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
597+ .collect(),
598+ only: None,
599+ trigger_kind: Some(CodeActionTriggerKind::INVOKED),
600+ };
601+ let code_action_request =
602+ language_server.code_actions(doc.identifier(), range, code_action_context)?;
603+ Some((code_action_request, offset_encoding, language_server_id))
604+ })
605+ .map(|(request, offset_encoding, ls_id)| async move {
606+ let json = request.await?;
607+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
585608 let mut actions = match response {
586609 Some(a) => a,
587− None => return,
610+ None => return anyhow::Ok(Vec::new()),
588611 };
589612
590613 // remove disabled code actions
@@ -596,11 +619,6 @@ pub fn code_action(cx: &mut Context) {
596619 )
597620 });
598621
599− if actions.is_empty() {
600− editor.set_status("No code actions available");
601− return;
602− }
603−
604622 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
605623 // Many details are modeled after vscode because language servers are usually tested against it.
606624 // VScode sorts the codeaction two times:
@@ -636,18 +654,48 @@ pub fn code_action(cx: &mut Context) {
636654 .reverse()
637655 });
638656
639− let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
657+ Ok(actions
658+ .into_iter()
659+ .map(|lsp_item| CodeActionOrCommandItem {
660+ lsp_item,
661+ offset_encoding,
662+ language_server_id: ls_id,
663+ })
664+ .collect())
665+ })
666+ .collect();
667+
668+ if futures.is_empty() {
669+ cx.editor
670+ .set_error("No Language server does support code actions");
671+ return;
672+ }
673+
674+ cx.jobs.callback(async move {
675+ let mut actions = Vec::new();
676+ // TODO if one code action request errors, all other requests are ignored (even if they're valid)
677+ while let Some(mut lsp_items) = futures.try_next().await? {
678+ actions.append(&mut lsp_items);
679+ }
680+
681+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
682+ if actions.is_empty() {
683+ editor.set_error("No code actions available");
684+ return;
685+ }
686+ let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
640687 if event != PromptEvent::Validate {
641688 return;
642689 }
643690
644691 // always present here
645− let code_action = code_action.unwrap();
692+ let action = action.unwrap();
693+ let offset_encoding = action.offset_encoding;
646694
647− match code_action {
695+ match &action.lsp_item {
648696 lsp::CodeActionOrCommand::Command(command) => {
649697 log::debug!("code action command: {:?}", command);
650− execute_lsp_command(editor, command.clone());
698+ execute_lsp_command(editor, action.language_server_id, command.clone());
651699 }
652700 lsp::CodeActionOrCommand::CodeAction(code_action) => {
653701 log::debug!("code action: {:?}", code_action);
@@ -659,7 +707,7 @@ pub fn code_action(cx: &mut Context) {
659707 // if code action provides both edit and command first the edit
660708 // should be applied and then the command
661709 if let Some(command) = &code_action.command {
662− execute_lsp_command(editor, command.clone());
710+ execute_lsp_command(editor, action.language_server_id, command.clone());
663711 }
664712 }
665713 }
@@ -668,8 +716,10 @@ pub fn code_action(cx: &mut Context) {
668716
669717 let popup = Popup::new("code-action", picker).with_scrollbar(false);
670718 compositor.replace_or_push("code-action", popup);
671− },
672− )
719+ };
720+
721+ Ok(Callback::EditorCompositor(Box::new(call)))
722+ });
673723}
674724
675725impl ui::menu::Item for lsp::Command {
@@ -679,13 +729,14 @@ impl ui::menu::Item for lsp::Command {
679729 }
680730}
681731
682−pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
683− let doc = doc!(editor);
684− let language_server = language_server!(editor, doc);
685−
732+pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
686733 // the command is executed on the server and communicated back
687734 // to the client asynchronously using workspace edits
688− let future = match language_server.command(cmd) {
735+ let future = match editor
736+ .language_servers
737+ .get_by_id(language_server_id)
738+ .and_then(|language_server| language_server.command(cmd))
739+ {
689740 Some(future) => future,
690741 None => {
691742 editor.set_error("Language server does not support executing commands");
@@ -977,18 +1028,22 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
9771028 }
9781029}
9791030
1031+// TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity...
9801032pub fn goto_declaration(cx: &mut Context) {
9811033 let (view, doc) = current!(cx.editor);
982− let language_server = language_server!(cx.editor, doc);
983− let offset_encoding = language_server.offset_encoding();
984−
985− let pos = doc.position(view.id, offset_encoding);
986−
987− let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
988− Some(future) => future,
1034+ let (future, offset_encoding) = match doc
1035+ .language_servers_with_feature(LanguageServerFeature::GotoDeclaration)
1036+ .iter()
1037+ .find_map(|language_server| {
1038+ let offset_encoding = language_server.offset_encoding();
1039+ let pos = doc.position(view.id, offset_encoding);
1040+ let future = language_server.goto_declaration(doc.identifier(), pos, None)?;
1041+ Some((future, offset_encoding))
1042+ }) {
1043+ Some(future_offset_encoding) => future_offset_encoding,
9891044 None => {
9901045 cx.editor
991− .set_error("Language server does not support goto-declaration");
1046+ .set_error("No language server supports goto-declaration");
9921047 return;
9931048 }
9941049 };
@@ -1004,16 +1059,19 @@ pub fn goto_declaration(cx: &mut Context) {
10041059
10051060pub fn goto_definition(cx: &mut Context) {
10061061 let (view, doc) = current!(cx.editor);
1007− let language_server = language_server!(cx.editor, doc);
1008− let offset_encoding = language_server.offset_encoding();
1009−
1010− let pos = doc.position(view.id, offset_encoding);
1011−
1012− let future = match language_server.goto_definition(doc.identifier(), pos, None) {
1013− Some(future) => future,
1062+ let (future, offset_encoding) = match doc
1063+ .language_servers_with_feature(LanguageServerFeature::GotoDefinition)
1064+ .iter()
1065+ .find_map(|language_server| {
1066+ let offset_encoding = language_server.offset_encoding();
1067+ let pos = doc.position(view.id, offset_encoding);
1068+ let future = language_server.goto_definition(doc.identifier(), pos, None)?;
1069+ Some((future, offset_encoding))
1070+ }) {
1071+ Some(future_offset_encoding) => future_offset_encoding,
10141072 None => {
10151073 cx.editor
1016− .set_error("Language server does not support goto-definition");
1074+ .set_error("No language server supports goto-definition");
10171075 return;
10181076 }
10191077 };
@@ -1029,16 +1087,19 @@ pub fn goto_definition(cx: &mut Context) {
10291087
10301088pub fn goto_type_definition(cx: &mut Context) {
10311089 let (view, doc) = current!(cx.editor);
1032− let language_server = language_server!(cx.editor, doc);
1033− let offset_encoding = language_server.offset_encoding();
1034−
1035− let pos = doc.position(view.id, offset_encoding);
1036−
1037− let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
1038− Some(future) => future,
1090+ let (future, offset_encoding) = match doc
1091+ .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition)
1092+ .iter()
1093+ .find_map(|language_server| {
1094+ let offset_encoding = language_server.offset_encoding();
1095+ let pos = doc.position(view.id, offset_encoding);
1096+ let future = language_server.goto_type_definition(doc.identifier(), pos, None)?;
1097+ Some((future, offset_encoding))
1098+ }) {
1099+ Some(future_offset_encoding) => future_offset_encoding,
10391100 None => {
10401101 cx.editor
1041− .set_error("Language server does not support goto-type-definition");
1102+ .set_error("No language server supports goto-type-definition");
10421103 return;
10431104 }
10441105 };
@@ -1054,16 +1115,19 @@ pub fn goto_type_definition(cx: &mut Context) {
10541115
10551116pub fn goto_implementation(cx: &mut Context) {
10561117 let (view, doc) = current!(cx.editor);
1057− let language_server = language_server!(cx.editor, doc);
1058− let offset_encoding = language_server.offset_encoding();
1059−
1060− let pos = doc.position(view.id, offset_encoding);
1061−
1062− let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
1063− Some(future) => future,
1118+ let (future, offset_encoding) = match doc
1119+ .language_servers_with_feature(LanguageServerFeature::GotoImplementation)
1120+ .iter()
1121+ .find_map(|language_server| {
1122+ let offset_encoding = language_server.offset_encoding();
1123+ let pos = doc.position(view.id, offset_encoding);
1124+ let future = language_server.goto_implementation(doc.identifier(), pos, None)?;
1125+ Some((future, offset_encoding))
1126+ }) {
1127+ Some(future_offset_encoding) => future_offset_encoding,
10641128 None => {
10651129 cx.editor
1066− .set_error("Language server does not support goto-implementation");
1130+ .set_error("no language server supports goto-implementation");
10671131 return;
10681132 }
10691133 };
@@ -1080,21 +1144,24 @@ pub fn goto_implementation(cx: &mut Context) {
10801144pub fn goto_reference(cx: &mut Context) {
10811145 let config = cx.editor.config();
10821146 let (view, doc) = current!(cx.editor);
1083− let language_server = language_server!(cx.editor, doc);
1084− let offset_encoding = language_server.offset_encoding();
1085−
1086− let pos = doc.position(view.id, offset_encoding);
1087−
1088− let future = match language_server.goto_reference(
1089− doc.identifier(),
1090− pos,
1091− config.lsp.goto_reference_include_declaration,
1092− None,
1093− ) {
1094− Some(future) => future,
1147+ let (future, offset_encoding) = match doc
1148+ .language_servers_with_feature(LanguageServerFeature::GotoReference)
1149+ .iter()
1150+ .find_map(|language_server| {
1151+ let offset_encoding = language_server.offset_encoding();
1152+ let pos = doc.position(view.id, offset_encoding);
1153+ let future = language_server.goto_reference(
1154+ doc.identifier(),
1155+ pos,
1156+ config.lsp.goto_reference_include_declaration,
1157+ None,
1158+ )?;
1159+ Some((future, offset_encoding))
1160+ }) {
1161+ Some(future_offset_encoding) => future_offset_encoding,
10951162 None => {
10961163 cx.editor
1097− .set_error("Language server does not support goto-reference");
1164+ .set_error("language server supports goto-reference");
10981165 return;
10991166 }
11001167 };
@@ -1108,7 +1175,7 @@ pub fn goto_reference(cx: &mut Context) {
11081175 );
11091176}
11101177
1111−#[derive(PartialEq, Eq)]
1178+#[derive(PartialEq, Eq, Clone, Copy)]
11121179pub enum SignatureHelpInvoked {
11131180 Manual,
11141181 Automatic,
@@ -1120,35 +1187,34 @@ pub fn signature_help(cx: &mut Context) {
11201187
11211188pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11221189 let (view, doc) = current!(cx.editor);
1123− let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
11241190
1125− let language_server = match doc.language_server() {
1126− Some(language_server) => language_server,
1191+ // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
1192+ let future = match doc
1193+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
1194+ .iter()
1195+ .find_map(|language_server| {
1196+ let pos = doc.position(view.id, language_server.offset_encoding());
1197+ language_server.text_document_signature_help(doc.identifier(), pos, None)
1198+ }) {
1199+ Some(future) => future.boxed(),
11271200 None => {
11281201 // Do not show the message if signature help was invoked
11291202 // automatically on backspace, trigger characters, etc.
1130− if was_manually_invoked {
1203+ if invoked == SignatureHelpInvoked::Manual {
11311204 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−
1139− let pos = doc.position(view.id, offset_encoding);
1140−
1141− let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
1142− Some(f) => f,
1143− None => {
1144− if was_manually_invoked {
1145− cx.editor
1146− .set_error("Language server does not support signature-help");
1205+ .set_error("No language server supports signature-help");
11471206 }
11481207 return;
11491208 }
11501209 };
1210+ signature_help_impl_with_future(cx, future, invoked);
1211+}
11511212
1213+pub fn signature_help_impl_with_future(
1214+ cx: &mut Context,
1215+ future: BoxFuture<'static, helix_lsp::Result<Value>>,
1216+ invoked: SignatureHelpInvoked,
1217+) {
11521218 cx.callback(
11531219 future,
11541220 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@@ -1156,7 +1222,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11561222
11571223 if !(config.lsp.auto_signature_help
11581224 || SignatureHelp::visible_popup(compositor).is_some()
1159− || was_manually_invoked)
1225+ || invoked == SignatureHelpInvoked::Manual)
11601226 {
11611227 return;
11621228 }
@@ -1165,7 +1231,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
11651231 // it very probably means the server was a little slow to respond and the user has
11661232 // already moved on to something else, making a signature help popup will just be an
11671233 // annoyance, see https://github.com/helix-editor/helix/issues/3112
1168− if !was_manually_invoked && editor.mode != Mode::Insert {
1234+ if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
11691235 return;
11701236 }
11711237
@@ -1255,18 +1321,20 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
12551321
12561322pub fn hover(cx: &mut Context) {
12571323 let (view, doc) = current!(cx.editor);
1258− let language_server = language_server!(cx.editor, doc);
1259− let offset_encoding = language_server.offset_encoding();
12601324
12611325 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
1326+ let request = doc
1327+ .language_servers_with_feature(LanguageServerFeature::Hover)
1328+ .iter()
1329+ .find_map(|language_server| {
1330+ let pos = doc.position(view.id, language_server.offset_encoding());
1331+ language_server.text_document_hover(doc.identifier(), pos, None)
1332+ });
12621333
1263− let pos = doc.position(view.id, offset_encoding);
1264−
1265− let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
1334+ let future = match request {
12661335 Some(future) => future,
12671336 None => {
1268− cx.editor
1269− .set_error("Language server does not support hover");
1337+ cx.editor.set_error("No language server supports hover");
12701338 return;
12711339 }
12721340 };
@@ -1349,7 +1417,11 @@ pub fn rename_symbol(cx: &mut Context) {
13491417 }
13501418 }
13511419
1352− fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
1420+ fn create_rename_prompt(
1421+ editor: &Editor,
1422+ prefill: String,
1423+ language_server_id: Option<usize>,
1424+ ) -> Box<ui::Prompt> {
13531425 let prompt = ui::Prompt::new(
13541426 "rename-to:".into(),
13551427 None,
@@ -1358,27 +1430,36 @@ pub fn rename_symbol(cx: &mut Context) {
13581430 if event != PromptEvent::Validate {
13591431 return;
13601432 }
1361−
13621433 let (view, doc) = current!(cx.editor);
1363− let language_server = language_server!(cx.editor, doc);
1364− let offset_encoding = language_server.offset_encoding();
1365−
1366− let pos = doc.position(view.id, offset_encoding);
1367−
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;
1434+ let request = doc
1435+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1436+ .iter()
1437+ .find_map(|language_server| {
1438+ if let Some(language_server_id) = language_server_id {
1439+ if language_server.id() != language_server_id {
1440+ return None;
1441+ }
13751442 }
1376− };
1377− match block_on(future) {
1378− Ok(edits) => {
1379− let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
1443+ let offset_encoding = language_server.offset_encoding();
1444+ let pos = doc.position(view.id, offset_encoding);
1445+ let future = language_server.rename_symbol(
1446+ doc.identifier(),
1447+ pos,
1448+ input.to_string(),
1449+ )?;
1450+ Some((future, offset_encoding))
1451+ });
1452+
1453+ if let Some((future, offset_encoding)) = request {
1454+ match block_on(future) {
1455+ Ok(edits) => {
1456+ let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
1457+ }
1458+ Err(err) => cx.editor.set_error(err.to_string()),
13801459 }
1381− Err(err) => cx.editor.set_error(err.to_string()),
1460+ } else {
1461+ cx.editor
1462+ .set_error("No language server supports symbol renaming");
13821463 }
13831464 },
13841465 )
@@ -1388,20 +1469,20 @@ pub fn rename_symbol(cx: &mut Context) {
13881469 }
13891470
13901471 let (view, doc) = current!(cx.editor);
1391− let language_server = language_server!(cx.editor, doc);
1392− let offset_encoding = language_server.offset_encoding();
1393−
1394− if !language_server.supports_rename() {
1395− cx.editor
1396− .set_error("Language server does not support symbol renaming");
1397− return;
1398− }
13991472
1400− let pos = doc.position(view.id, offset_encoding);
1473+ let prepare_rename_request = doc
1474+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1475+ .iter()
1476+ .find_map(|language_server| {
1477+ let offset_encoding = language_server.offset_encoding();
1478+ let pos = doc.position(view.id, offset_encoding);
1479+ let future = language_server.prepare_rename(doc.identifier(), pos)?;
1480+ Some((future, offset_encoding, language_server.id()))
1481+ });
14011482
1402− match language_server.prepare_rename(doc.identifier(), pos) {
1483+ match prepare_rename_request {
14031484 // Language server supports textDocument/prepareRename, use it.
1404− Some(future) => cx.callback(
1485+ Some((future, offset_encoding, ls_id)) => cx.callback(
14051486 future,
14061487 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
14071488 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1413,7 +1494,7 @@ pub fn rename_symbol(cx: &mut Context) {
14131494 }
14141495 };
14151496
1416− let prompt = create_rename_prompt(editor, prefill);
1497+ let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
14171498
14181499 compositor.push(prompt);
14191500 },
@@ -1423,7 +1504,7 @@ pub fn rename_symbol(cx: &mut Context) {
14231504 None => {
14241505 let prefill = get_prefill_from_word_boundary(cx.editor);
14251506
1426− let prompt = create_rename_prompt(cx.editor, prefill);
1507+ let prompt = create_rename_prompt(cx.editor, prefill, None);
14271508
14281509 cx.push_layer(prompt);
14291510 }
@@ -1432,17 +1513,20 @@ pub fn rename_symbol(cx: &mut Context) {
14321513
14331514pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
14341515 let (view, doc) = current!(cx.editor);
1435− let language_server = language_server!(cx.editor, doc);
1436− let offset_encoding = language_server.offset_encoding();
1437−
1438− let pos = doc.position(view.id, offset_encoding);
1439−
1440− let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
1441− {
1516+ let (future, offset_encoding) = match doc
1517+ .language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
1518+ .iter()
1519+ .find_map(|language_server| {
1520+ let offset_encoding = language_server.offset_encoding();
1521+ let pos = doc.position(view.id, offset_encoding);
1522+ let future =
1523+ language_server.text_document_document_highlight(doc.identifier(), pos, None)?;
1524+ Some((future, offset_encoding))
1525+ }) {
14421526 Some(future) => future,
14431527 None => {
14441528 cx.editor
1445− .set_error("Language server does not support document highlight");
1529+ .set_error("No language server supports document-highlight");
14461530 return;
14471531 }
14481532 };
@@ -1455,8 +1539,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
14551539 _ => return,
14561540 };
14571541 let (view, doc) = current!(editor);
1458− let language_server = language_server!(editor, doc);
1459− let offset_encoding = language_server.offset_encoding();
14601542 let text = doc.text();
14611543 let pos = doc.selection(view.id).primary().head;
14621544
@@ -1502,63 +1584,58 @@ fn compute_inlay_hints_for_view(
15021584 let view_id = view.id;
15031585 let doc_id = view.doc;
15041586
1505− let language_server = doc.language_server()?;
1506−
1507− let capabilities = language_server.capabilities();
1508−
1509− let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
1510− Some(
1511− lsp::OneOf::Left(true)
1512− | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
1513− ) => {
1514− let doc_text = doc.text();
1515− let len_lines = doc_text.len_lines();
1516−
1517− // Compute ~3 times the current view height of inlay hints, that way some scrolling
1518− // will not show half the view with hints and half without while still being faster
1519− // than computing all the hints for the full file (which could be dozens of time
1520− // longer than the view is).
1521− let view_height = view.inner_height();
1522− let first_visible_line =
1523− doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1524− let first_line = first_visible_line.saturating_sub(view_height);
1525− let last_line = first_visible_line
1526− .saturating_add(view_height.saturating_mul(2))
1527− .min(len_lines);
1528−
1529− let new_doc_inlay_hint_id = DocumentInlayHintsId {
1530− first_line,
1531− last_line,
1532− };
1533− // Don't recompute the annotations in case nothing has changed about the view
1534− if !doc.inlay_hints_oudated
1535− && doc
1536− .inlay_hints(view_id)
1537− .map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
1538− {
1539− return None;
1540− }
1587+ let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints);
1588+ let language_server = language_servers.iter().find(|language_server| {
1589+ matches!(
1590+ language_server.capabilities().inlay_hint_provider,
1591+ Some(
1592+ lsp::OneOf::Left(true)
1593+ | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
1594+ )
1595+ )
1596+ })?;
1597+
1598+ let doc_text = doc.text();
1599+ let len_lines = doc_text.len_lines();
1600+
1601+ // Compute ~3 times the current view height of inlay hints, that way some scrolling
1602+ // will not show half the view with hints and half without while still being faster
1603+ // than computing all the hints for the full file (which could be dozens of time
1604+ // longer than the view is).
1605+ let view_height = view.inner_height();
1606+ let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1607+ let first_line = first_visible_line.saturating_sub(view_height);
1608+ let last_line = first_visible_line
1609+ .saturating_add(view_height.saturating_mul(2))
1610+ .min(len_lines);
1611+
1612+ let new_doc_inlay_hints_id = DocumentInlayHintsId {
1613+ first_line,
1614+ last_line,
1615+ };
1616+ // Don't recompute the annotations in case nothing has changed about the view
1617+ if !doc.inlay_hints_oudated
1618+ && doc
1619+ .inlay_hints(view_id)
1620+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
1621+ {
1622+ return None;
1623+ }
15411624
1542− let doc_slice = doc_text.slice(..);
1543− let first_char_in_range = doc_slice.line_to_char(first_line);
1544− let last_char_in_range = doc_slice.line_to_char(last_line);
1625+ let doc_slice = doc_text.slice(..);
1626+ let first_char_in_range = doc_slice.line_to_char(first_line);
1627+ let last_char_in_range = doc_slice.line_to_char(last_line);
15451628
1546− let range = helix_lsp::util::range_to_lsp_range(
1547− doc_text,
1548− helix_core::Range::new(first_char_in_range, last_char_in_range),
1549− language_server.offset_encoding(),
1550− );
1629+ let range = helix_lsp::util::range_to_lsp_range(
1630+ doc_text,
1631+ helix_core::Range::new(first_char_in_range, last_char_in_range),
1632+ language_server.offset_encoding(),
1633+ );
15511634
1552− (
1553− language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
1554− new_doc_inlay_hint_id,
1555− )
1556− }
1557− _ => return None,
1558− };
1635+ let offset_encoding = language_server.offset_encoding();
15591636
15601637 let callback = super::make_job_callback(
1561− future?,
1638+ language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
15621639 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
15631640 // The config was modified or the window was closed while the request was in flight
15641641 if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
@@ -1572,8 +1649,8 @@ fn compute_inlay_hints_for_view(
15721649 };
15731650
15741651 // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
1575− let (mut hints, offset_encoding) = match (response, doc.language_server()) {
1576− (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
1652+ let mut hints = match response {
1653+ Some(hints) if !hints.is_empty() => hints,
15771654 _ => {
15781655 doc.set_inlay_hints(
15791656 view_id,
helix-term/src/commands/typed.rs+73−33
@@ -1329,23 +1329,20 @@ fn lsp_workspace_command(
13291329 if event != PromptEvent::Validate {
13301330 return Ok(());
13311331 }
1332−
1333− let (_, doc) = current!(cx.editor);
1334−
1335− let language_server = match doc.language_server() {
1336− Some(language_server) => language_server,
1337− None => {
1338− cx.editor
1339− .set_status("Language server not active for current buffer");
1340− return Ok(());
1341− }
1342− };
1343−
1344− let options = match &language_server.capabilities().execute_command_provider {
1345− Some(options) => options,
1332+ let doc = doc!(cx.editor);
1333+ let language_servers =
1334+ doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
1335+ let (language_server_id, options) = match language_servers.iter().find_map(|ls| {
1336+ ls.capabilities()
1337+ .execute_command_provider
1338+ .as_ref()
1339+ .map(|options| (ls.id(), options))
1340+ }) {
1341+ Some(id_options) => id_options,
13461342 None => {
1347− cx.editor
1348− .set_status("Workspace commands are not supported for this language server");
1343+ cx.editor.set_status(
1344+ "No active language servers for this document support workspace commands",
1345+ );
13491346 return Ok(());
13501347 }
13511348 };
@@ -1362,8 +1359,8 @@ fn lsp_workspace_command(
13621359 let callback = async move {
13631360 let call: job::Callback = Callback::EditorCompositor(Box::new(
13641361 move |_editor: &mut Editor, compositor: &mut Compositor| {
1365− let picker = ui::Picker::new(commands, (), |cx, command, _action| {
1366− execute_lsp_command(cx.editor, command.clone());
1362+ let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
1363+ execute_lsp_command(cx.editor, language_server_id, command.clone());
13671364 });
13681365 compositor.push(Box::new(overlaid(picker)))
13691366 },
@@ -1376,6 +1373,7 @@ fn lsp_workspace_command(
13761373 if options.commands.iter().any(|c| c == &command) {
13771374 execute_lsp_command(
13781375 cx.editor,
1376+ language_server_id,
13791377 helix_lsp::lsp::Command {
13801378 title: command.clone(),
13811379 arguments: None,
@@ -1426,7 +1424,7 @@ fn lsp_restart(
14261424 .collect();
14271425
14281426 for document_id in document_ids_to_refresh {
1429− cx.editor.refresh_language_server(document_id);
1427+ cx.editor.refresh_language_servers(document_id);
14301428 }
14311429
14321430 Ok(())
@@ -1443,21 +1441,63 @@ fn lsp_stop(
14431441
14441442 let doc = doc!(cx.editor);
14451443
1446− let ls_id = doc
1447− .language_server()
1448− .map(|ls| ls.id())
1449− .context("LSP not running for the current document")?;
1444+ // TODO this stops language servers which may be used in another doc/language type that uses the same language servers
1445+ // I'm not sure if this is really what we want
1446+ let ls_shutdown_names = doc
1447+ .language_servers()
1448+ .iter()
1449+ .map(|ls| ls.name())
1450+ .collect::<Vec<_>>();
14501451
1451− let config = doc
1452− .language_config()
1453− .context("LSP not defined for the current document")?;
1454− cx.editor.language_servers.stop(config);
1452+ for ls_name in &ls_shutdown_names {
1453+ cx.editor.language_servers.stop(ls_name);
1454+ }
1455+
1456+ let doc_ids_active_clients: Vec<_> = cx
1457+ .editor
1458+ .documents()
1459+ .filter_map(|doc| {
1460+ let doc_active_ls_ids: Vec<_> = doc
1461+ .language_servers()
1462+ .iter()
1463+ .filter(|ls| !ls_shutdown_names.contains(&ls.name()))
1464+ .map(|ls| ls.id())
1465+ .collect();
1466+
1467+ let active_clients: Vec<_> = cx
1468+ .editor
1469+ .language_servers
1470+ .iter_clients()
1471+ .filter(|client| doc_active_ls_ids.contains(&client.id()))
1472+ .map(Clone::clone)
1473+ .collect();
1474+
1475+ if active_clients.len() != doc.language_servers().len() {
1476+ Some((doc.id(), active_clients))
1477+ } else {
1478+ None
1479+ }
1480+ })
1481+ .collect();
1482+
1483+ for (doc_id, active_clients) in doc_ids_active_clients {
1484+ let doc = cx.editor.documents.get_mut(&doc_id).unwrap();
1485+
1486+ let stopped_clients: Vec<_> = doc
1487+ .language_servers()
1488+ .iter()
1489+ .filter(|ls| {
1490+ !active_clients
1491+ .iter()
1492+ .any(|active_ls| active_ls.id() == ls.id())
1493+ })
1494+ .map(|ls| ls.id())
1495+ .collect(); // is necessary because of borrow-checking
14551496
1456− for doc in cx.editor.documents_mut() {
1457− if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
1458− doc.set_language_server(None);
1459− doc.set_diagnostics(Default::default());
1497+ for client_id in stopped_clients {
1498+ doc.clear_diagnostics(client_id)
14601499 }
1500+ doc.set_language_servers(active_clients);
14611501 }
14621502
14631503 Ok(())
@@ -1850,7 +1890,7 @@ fn language(
18501890 doc.detect_indent_and_line_ending();
18511891
18521892 let id = doc.id();
1853− cx.editor.refresh_language_server(id);
1893+ cx.editor.refresh_language_servers(id);
18541894 Ok(())
18551895}
18561896
@@ -2588,7 +2628,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
25882628 TypableCommand {
25892629 name: "lsp-restart",
25902630 aliases: &[],
2591− doc: "Restarts the Language Server that is in use by the current doc",
2631+ doc: "Restarts the language servers used by the current doc",
25922632 fun: lsp_restart,
25932633 signature: CommandSignature::none(),
25942634 },
helix-term/src/health.rs+19−8
@@ -2,7 +2,10 @@ use crossterm::{
22 style::{Color, Print, Stylize},
33 tty::IsTty,
44};
5−use helix_core::config::{default_syntax_loader, user_syntax_loader};
5+use helix_core::{
6+ config::{default_syntax_loader, user_syntax_loader},
7+ syntax::LanguageServerFeatureConfiguration,
8+};
69use helix_loader::grammar::load_runtime_file;
710use helix_view::clipboard::get_clipboard_provider;
811use std::io::Write;
@@ -192,10 +195,14 @@ pub fn languages_all() -> std::io::Result<()> {
192195 for lang in &syn_loader_conf.language {
193196 column(&lang.language_id, Color::Reset);
194197
195− let lsp = lang
196− .language_server
197− .as_ref()
198− .map(|lsp| lsp.command.to_string());
198+ // TODO multiple language servers (check binary for each supported language server, not just the first)
199+
200+ let lsp = lang.language_servers.first().and_then(|lsp| {
201+ syn_loader_conf
202+ .language_server
203+ .get(lsp.name())
204+ .map(|config| config.command.clone())
205+ });
199206 check_binary(lsp);
200207
201208 let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@@ -264,11 +271,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
264271 }
265272 };
266273
274+ // TODO multiple language servers
267275 probe_protocol(
268276 "language server",
269− lang.language_server
270− .as_ref()
271− .map(|lsp| lsp.command.to_string()),
277+ lang.language_servers.first().and_then(|lsp| {
278+ syn_loader_conf
279+ .language_server
280+ .get(lsp.name())
281+ .map(|config| config.command.clone())
282+ }),
272283 )?;
273284
274285 probe_protocol(
helix-term/src/ui/completion.rs+42−43
@@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
1515use crate::commands;
1616use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
1717
18−use helix_lsp::{lsp, util};
18+use helix_lsp::{lsp, util, OffsetEncoding};
1919
2020impl menu::Item for CompletionItem {
2121 type Data = ();
@@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
3838 || self.item.tags.as_ref().map_or(false, |tags| {
3939 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
4040 });
41+
4142 menu::Row::new(vec![
4243 menu::Cell::from(Span::styled(
4344 self.item.label.as_str(),
@@ -79,19 +80,16 @@ impl menu::Item for CompletionItem {
7980 }
8081 None => "",
8182 }),
82− // self.detail.as_deref().unwrap_or("")
83− // self.label_details
84− // .as_ref()
85− // .or(self.detail())
86− // .as_str(),
8783 ])
8884 }
8985}
9086
9187#[derive(Debug, PartialEq, Default, Clone)]
92−struct CompletionItem {
93− item: lsp::CompletionItem,
94− resolved: bool,
88+pub struct CompletionItem {
89+ pub item: lsp::CompletionItem,
90+ pub language_server_id: usize,
91+ pub offset_encoding: OffsetEncoding,
92+ pub resolved: bool,
9593}
9694
9795/// Wraps a Menu.
@@ -109,21 +107,13 @@ impl Completion {
109107 pub fn new(
110108 editor: &Editor,
111109 savepoint: Arc<SavePoint>,
112− mut items: Vec<lsp::CompletionItem>,
113− offset_encoding: helix_lsp::OffsetEncoding,
110+ mut items: Vec<CompletionItem>,
114111 start_offset: usize,
115112 trigger_offset: usize,
116113 ) -> Self {
117114 let replace_mode = editor.config().completion_replace;
118115 // Sort completion items according to their preselect status (given by the LSP server)
119− items.sort_by_key(|item| !item.preselect.unwrap_or(false));
120− let items = items
121− .into_iter()
122− .map(|item| CompletionItem {
123− item,
124− resolved: false,
125− })
126− .collect();
116+ items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
127117
128118 // Then create the menu
129119 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
@@ -131,7 +121,6 @@ impl Completion {
131121 doc: &Document,
132122 view_id: ViewId,
133123 item: &CompletionItem,
134− offset_encoding: helix_lsp::OffsetEncoding,
135124 trigger_offset: usize,
136125 include_placeholder: bool,
137126 replace_mode: bool,
@@ -154,6 +143,8 @@ impl Completion {
154143 }
155144 };
156145
146+ let offset_encoding = item.offset_encoding;
147+
157148 let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
158149 return Transaction::new(doc.text());
159150 };
@@ -247,15 +238,8 @@ impl Completion {
247238 // always present here
248239 let item = item.unwrap();
249240
250− let transaction = item_to_transaction(
251− doc,
252− view.id,
253− item,
254− offset_encoding,
255− trigger_offset,
256− true,
257− replace_mode,
258− );
241+ let transaction =
242+ item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode);
259243 doc.apply_temporary(&transaction, view.id);
260244 }
261245 PromptEvent::Validate => {
@@ -267,10 +251,15 @@ impl Completion {
267251 // always present here
268252 let mut item = item.unwrap().clone();
269253
254+ let language_server = editor
255+ .language_servers
256+ .get_by_id(item.language_server_id)
257+ .unwrap();
258+
270259 // resolve item if not yet resolved
271260 if !item.resolved {
272261 if let Some(resolved) =
273− Self::resolve_completion_item(doc, item.item.clone())
262+ Self::resolve_completion_item(language_server, item.item.clone())
274263 {
275264 item.item = resolved;
276265 }
@@ -281,7 +270,6 @@ impl Completion {
281270 doc,
282271 view.id,
283272 &item,
284− offset_encoding,
285273 trigger_offset,
286274 false,
287275 replace_mode,
@@ -299,7 +287,7 @@ impl Completion {
299287 let transaction = util::generate_transaction_from_edits(
300288 doc.text(),
301289 additional_edits,
302− offset_encoding, // TODO: should probably transcode in Client
290+ item.offset_encoding, // TODO: should probably transcode in Client
303291 );
304292 doc.apply(&transaction, view.id);
305293 }
@@ -323,10 +311,17 @@ impl Completion {
323311 }
324312
325313 fn resolve_completion_item(
326− doc: &Document,
314+ language_server: &helix_lsp::Client,
327315 completion_item: lsp::CompletionItem,
328316 ) -> Option<lsp::CompletionItem> {
329− let language_server = doc.language_server()?;
317+ let completion_resolve_provider = language_server
318+ .capabilities()
319+ .completion_provider
320+ .as_ref()?
321+ .resolve_provider;
322+ if completion_resolve_provider != Some(true) {
323+ return None;
324+ }
330325
331326 let future = language_server.resolve_completion_item(completion_item)?;
332327 let response = helix_lsp::block_on(future);
@@ -397,8 +392,11 @@ impl Completion {
397392 Some(item) if !item.resolved => item.clone(),
398393 _ => return false,
399394 };
400−
401− let language_server = match doc!(cx.editor).language_server() {
395+ let language_server = match cx
396+ .editor
397+ .language_servers
398+ .get_by_id(current_item.language_server_id)
399+ {
402400 Some(language_server) => language_server,
403401 None => return false,
404402 };
@@ -422,13 +420,14 @@ impl Completion {
422420 .unwrap()
423421 .completion
424422 {
425− completion.replace_item(
426− current_item,
427− CompletionItem {
428− item: resolved_item,
429− resolved: true,
430− },
431− );
423+ let resolved_item = CompletionItem {
424+ item: resolved_item,
425+ language_server_id: current_item.language_server_id,
426+ offset_encoding: current_item.offset_encoding,
427+ resolved: true,
428+ };
429+
430+ completion.replace_item(current_item, resolved_item);
432431 }
433432 },
434433 );
helix-term/src/ui/editor.rs+5−12
@@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
3333
3434use tui::{buffer::Buffer as Surface, text::Span};
3535
36−use super::statusline;
36+use super::{completion::CompletionItem, statusline};
3737use super::{document::LineDecoration, lsp::SignatureHelp};
3838
3939pub struct EditorView {
@@ -650,7 +650,7 @@ impl EditorView {
650650 .primary()
651651 .cursor(doc.text().slice(..));
652652
653− let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
653+ let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
654654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
655655 });
656656
@@ -953,20 +953,13 @@ impl EditorView {
953953 &mut self,
954954 editor: &mut Editor,
955955 savepoint: Arc<SavePoint>,
956− items: Vec<helix_lsp::lsp::CompletionItem>,
957− offset_encoding: helix_lsp::OffsetEncoding,
956+ items: Vec<CompletionItem>,
958957 start_offset: usize,
959958 trigger_offset: usize,
960959 size: Rect,
961960 ) -> Option<Rect> {
962− let mut completion = Completion::new(
963− editor,
964− savepoint,
965− items,
966− offset_encoding,
967− start_offset,
968− trigger_offset,
969− );
961+ let mut completion =
962+ Completion::new(editor, savepoint, items, start_offset, trigger_offset);
970963
971964 if completion.is_empty() {
972965 // skip if we got no completion results
helix-term/src/ui/mod.rs+9−12
@@ -17,7 +17,7 @@ mod text;
1717use crate::compositor::{Component, Compositor};
1818use crate::filter_picker_entry;
1919use crate::job::{self, Callback};
20−pub use completion::Completion;
20+pub use completion::{Completion, CompletionItem};
2121pub use editor::EditorView;
2222pub use markdown::Markdown;
2323pub use menu::Menu;
@@ -238,6 +238,7 @@ pub mod completers {
238238 use crate::ui::prompt::Completion;
239239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
240240 use fuzzy_matcher::FuzzyMatcher;
241+ use helix_core::syntax::LanguageServerFeature;
241242 use helix_view::document::SCRATCH_BUFFER_NAME;
242243 use helix_view::theme;
243244 use helix_view::{editor::Config, Editor};
@@ -393,17 +394,13 @@ pub mod completers {
393394 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394395 let matcher = Matcher::default();
395396
396− let (_, doc) = current_ref!(editor);
397−
398− let language_server = match doc.language_server() {
399− Some(language_server) => language_server,
400− None => {
401− return vec![];
402− }
403− };
404−
405− let options = match &language_server.capabilities().execute_command_provider {
406− Some(options) => options,
397+ let language_servers =
398+ doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
399+ let options = match language_servers
400+ .into_iter()
401+ .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
402+ {
403+ Some(id_options) => id_options,
407404 None => {
408405 return vec![];
409406 }
helix-term/src/ui/statusline.rs+6−6
@@ -197,15 +197,16 @@ where
197197 );
198198}
199199
200+// TODO think about handling multiple language servers
200201fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201202where
202203 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203204{
205+ let language_servers = context.doc.language_servers();
204206 write(
205207 context,
206− context
207− .doc
208− .language_server()
208+ language_servers
209+ .first()
209210 .and_then(|srv| {
210211 context
211212 .spinners
@@ -225,8 +226,7 @@ where
225226{
226227 let (warnings, errors) = context
227228 .doc
228− .diagnostics()
229− .iter()
229+ .shown_diagnostics()
230230 .fold((0, 0), |mut counts, diag| {
231231 use helix_core::diagnostic::Severity;
232232 match diag.severity {
@@ -266,7 +266,7 @@ where
266266 .diagnostics
267267 .values()
268268 .flatten()
269− .fold((0, 0), |mut counts, diag| {
269+ .fold((0, 0), |mut counts, (diag, _, _)| {
270270 match diag.severity {
271271 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
272272 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
helix-view/src/document.rs+104−41
@@ -6,7 +6,7 @@ use futures_util::FutureExt;
66use helix_core::auto_pairs::AutoPairs;
77use helix_core::doc_formatter::TextFormat;
88use helix_core::encoding::Encoding;
9−use helix_core::syntax::Highlight;
9+use helix_core::syntax::{Highlight, LanguageServerFeature, LanguageServerFeatureConfiguration};
1010use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
1111use helix_core::Range;
1212use helix_vcs::{DiffHandle, DiffProviderRegistry};
@@ -16,7 +16,7 @@ use serde::de::{self, Deserialize, Deserializer};
1616use serde::Serialize;
1717use std::borrow::Cow;
1818use std::cell::Cell;
19−use std::collections::HashMap;
19+use std::collections::{HashMap, HashSet};
2020use std::fmt::Display;
2121use std::future::Future;
2222use std::path::{Path, PathBuf};
@@ -180,7 +180,7 @@ pub struct Document {
180180 pub(crate) modified_since_accessed: bool,
181181
182182 diagnostics: Vec<Diagnostic>,
183− language_server: Option<Arc<helix_lsp::Client>>,
183+ language_servers: Vec<Arc<helix_lsp::Client>>,
184184
185185 diff_handle: Option<DiffHandle>,
186186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@@ -616,7 +616,7 @@ impl Document {
616616 last_saved_time: SystemTime::now(),
617617 last_saved_revision: 0,
618618 modified_since_accessed: false,
619− language_server: None,
619+ language_servers: Vec::new(),
620620 diff_handle: None,
621621 config,
622622 version_control_head: None,
@@ -730,19 +730,24 @@ impl Document {
730730 return Some(formatting_future.boxed());
731731 };
732732
733− let language_server = self.language_server()?;
734733 let text = self.text.clone();
735− let offset_encoding = language_server.offset_encoding();
736−
737− let request = language_server.text_document_formatting(
738− self.identifier(),
739− lsp::FormattingOptions {
740− tab_size: self.tab_width() as u32,
741− insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
742− ..Default::default()
743− },
744− None,
745− )?;
734+ // finds first language server that supports formatting and then formats
735+ let (offset_encoding, request) = self
736+ .language_servers_with_feature(LanguageServerFeature::Format)
737+ .iter()
738+ .find_map(|language_server| {
739+ let offset_encoding = language_server.offset_encoding();
740+ let request = language_server.text_document_formatting(
741+ self.identifier(),
742+ lsp::FormattingOptions {
743+ tab_size: self.tab_width() as u32,
744+ insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
745+ ..Default::default()
746+ },
747+ None,
748+ )?;
749+ Some((offset_encoding, request))
750+ })?;
746751
747752 let fut = async move {
748753 let edits = request.await.unwrap_or_else(|e| {
@@ -797,13 +802,12 @@ impl Document {
797802 if self.path.is_none() {
798803 bail!("Can't save with no path set!");
799804 }
800−
801805 self.path.as_ref().unwrap().clone()
802806 }
803807 };
804808
805809 let identifier = self.path().map(|_| self.identifier());
806− let language_server = self.language_server.clone();
810+ let language_servers = self.language_servers.clone();
807811
808812 // mark changes up to now as saved
809813 let current_rev = self.get_current_revision();
@@ -847,14 +851,13 @@ impl Document {
847851 text: text.clone(),
848852 };
849853
850− if let Some(language_server) = language_server {
854+ for language_server in language_servers {
851855 if !language_server.is_initialized() {
852856 return Ok(event);
853857 }
854−
855− if let Some(identifier) = identifier {
858+ if let Some(identifier) = &identifier {
856859 if let Some(notification) =
857− language_server.text_document_did_save(identifier, &text)
860+ language_server.text_document_did_save(identifier.clone(), &text)
858861 {
859862 notification.await?;
860863 }
@@ -1005,8 +1008,8 @@ impl Document {
10051008 }
10061009
10071010 /// Set the LSP.
1008− pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
1009− self.language_server = language_server;
1011+ pub fn set_language_servers(&mut self, language_servers: Vec<Arc<helix_lsp::Client>>) {
1012+ self.language_servers = language_servers;
10101013 }
10111014
10121015 /// Select text within the [`Document`].
@@ -1159,7 +1162,7 @@ impl Document {
11591162
11601163 if emit_lsp_notification {
11611164 // emit lsp notification
1162− if let Some(language_server) = self.language_server() {
1165+ for language_server in self.language_servers() {
11631166 let notify = language_server.text_document_did_change(
11641167 self.versioned_identifier(),
11651168 &old_doc,
@@ -1415,18 +1418,13 @@ impl Document {
14151418 .map(|language| language.language_id.as_str())
14161419 }
14171420
1418− /// Language ID for the document. Either the `language-id` from the
1419− /// `language-server` configuration, or the document language if no
1420− /// `language-id` has been specified.
1421+ /// Language ID for the document. Either the `language-id`,
1422+ /// or the document language name if no `language-id` has been specified.
14211423 pub fn language_id(&self) -> Option<&str> {
1422− let language_config = self.language.as_deref()?;
1423−
1424− language_config
1425− .language_server
1426− .as_ref()?
1427− .language_id
1424+ self.language_config()?
1425+ .language_server_language_id
14281426 .as_deref()
1429− .or(Some(language_config.language_id.as_str()))
1427+ .or_else(|| self.language_name())
14301428 }
14311429
14321430 /// Corresponding [`LanguageConfiguration`].
@@ -1439,10 +1437,54 @@ impl Document {
14391437 self.version
14401438 }
14411439
1442− /// Language server if it has been initialized.
1443− pub fn language_server(&self) -> Option<&helix_lsp::Client> {
1444− let server = self.language_server.as_deref()?;
1445− server.is_initialized().then_some(server)
1440+ /// Language servers that have been initialized.
1441+ pub fn language_servers(&self) -> Vec<&helix_lsp::Client> {
1442+ self.language_servers
1443+ .iter()
1444+ .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None })
1445+ .collect()
1446+ }
1447+
1448+ // TODO filter also based on LSP capabilities?
1449+ pub fn language_servers_with_feature(
1450+ &self,
1451+ feature: LanguageServerFeature,
1452+ ) -> Vec<&helix_lsp::Client> {
1453+ let language_servers = self.language_servers();
1454+
1455+ let language_config = match self.language_config() {
1456+ Some(language_config) => language_config,
1457+ None => return Vec::new(),
1458+ };
1459+
1460+ // O(n^2) but since language_servers will be of very small length,
1461+ // I don't see the necessity to optimize
1462+ language_config
1463+ .language_servers
1464+ .iter()
1465+ .filter_map(|c| match c {
1466+ LanguageServerFeatureConfiguration::Simple(name) => language_servers
1467+ .iter()
1468+ .find(|ls| ls.name() == name)
1469+ .copied(),
1470+ LanguageServerFeatureConfiguration::Features {
1471+ only_features,
1472+ except_features,
1473+ name,
1474+ } => {
1475+ if (only_features.is_empty() || only_features.contains(&feature))
1476+ && !except_features.contains(&feature)
1477+ {
1478+ language_servers
1479+ .iter()
1480+ .find(|ls| ls.name() == name)
1481+ .copied()
1482+ } else {
1483+ None
1484+ }
1485+ }
1486+ })
1487+ .collect()
14461488 }
14471489
14481490 pub fn diff_handle(&self) -> Option<&DiffHandle> {
@@ -1565,12 +1607,33 @@ impl Document {
15651607 &self.diagnostics
15661608 }
15671609
1568− pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
1569− self.diagnostics = diagnostics;
1610+ pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
1611+ let ls_ids: HashSet<_> = self
1612+ .language_servers_with_feature(LanguageServerFeature::Diagnostics)
1613+ .iter()
1614+ .map(|ls| ls.id())
1615+ .collect();
1616+ self.diagnostics
1617+ .iter()
1618+ .filter(move |d| ls_ids.contains(&d.language_server_id))
1619+ }
1620+
1621+ pub fn replace_diagnostics(
1622+ &mut self,
1623+ mut diagnostics: Vec<Diagnostic>,
1624+ language_server_id: usize,
1625+ ) {
1626+ self.clear_diagnostics(language_server_id);
1627+ self.diagnostics.append(&mut diagnostics);
15701628 self.diagnostics
15711629 .sort_unstable_by_key(|diagnostic| diagnostic.range);
15721630 }
15731631
1632+ pub fn clear_diagnostics(&mut self, language_server_id: usize) {
1633+ self.diagnostics
1634+ .retain(|d| d.language_server_id != language_server_id);
1635+ }
1636+
15741637 /// Get the document's auto pairs. If the document has a recognized
15751638 /// language config with auto pairs configured, returns that;
15761639 /// otherwise, falls back to the global auto pairs config. If the global
helix-view/src/editor.rs+36−28
@@ -48,7 +48,7 @@ use helix_core::{
4848};
4949use helix_core::{Position, Selection};
5050use helix_dap as dap;
51−use helix_lsp::lsp;
51+use helix_lsp::{lsp, OffsetEncoding};
5252
5353use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
5454
@@ -689,7 +689,7 @@ pub struct WhitespaceCharacters {
689689impl Default for WhitespaceCharacters {
690690 fn default() -> Self {
691691 Self {
692− space: '·', // U+00B7
692+ space: '·', // U+00B7
693693 nbsp: '⍽', // U+237D
694694 tab: '→', // U+2192
695695 newline: '⏎', // U+23CE
@@ -818,7 +818,7 @@ pub struct Editor {
818818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
819819 pub macro_replaying: Vec<char>,
820820 pub language_servers: helix_lsp::Registry,
821− pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
821+ pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>,
822822 pub diff_providers: DiffProviderRegistry,
823823
824824 pub debugger: Option<dap::Client>,
@@ -941,6 +941,7 @@ impl Editor {
941941 syn_loader: Arc<syntax::Loader>,
942942 config: Arc<dyn DynAccess<Config>>,
943943 ) -> Self {
944+ let language_servers = helix_lsp::Registry::new(syn_loader.clone());
944945 let conf = config.load();
945946 let auto_pairs = (&conf.auto_pairs).into();
946947
@@ -960,7 +961,7 @@ impl Editor {
960961 macro_recording: None,
961962 macro_replaying: Vec::new(),
962963 theme: theme_loader.default(),
963− language_servers: helix_lsp::Registry::new(),
964+ language_servers,
964965 diagnostics: BTreeMap::new(),
965966 diff_providers: DiffProviderRegistry::default(),
966967 debugger: None,
@@ -1093,12 +1094,12 @@ impl Editor {
10931094 }
10941095
10951096 /// Refreshes the language server for a given document
1096− pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1097− self.launch_language_server(doc_id)
1097+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1098+ self.launch_language_servers(doc_id)
10981099 }
10991100
11001101 /// Launch a language server for a given document
1101− fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1102+ fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
11021103 if !self.config().lsp.enable {
11031104 return None;
11041105 }
@@ -1109,42 +1110,49 @@ impl Editor {
11091110 let config = doc.config.load();
11101111 let root_dirs = &config.workspace_lsp_roots;
11111112
1112− // try to find a language server based on the language name
1113− let language_server = lang.as_ref().and_then(|language| {
1113+ // try to find language servers based on the language name
1114+ let language_servers = lang.as_ref().and_then(|language| {
11141115 self.language_servers
11151116 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
11161117 .map_err(|e| {
11171118 log::error!(
1118− "Failed to initialize the LSP for `{}` {{ {} }}",
1119+ "Failed to initialize the language servers for `{}` {{ {} }}",
11191120 language.scope(),
11201121 e
11211122 )
11221123 })
11231124 .ok()
1124− .flatten()
11251125 });
11261126
11271127 let doc = self.document_mut(doc_id)?;
11281128 let doc_url = doc.url()?;
11291129
1130− if let Some(language_server) = language_server {
1131− // only spawn a new lang server if the servers aren't the same
1132− if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
1133− if let Some(language_server) = doc.language_server() {
1134− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1130+ if let Some(language_servers) = language_servers {
1131+ // only spawn new lang servers if the servers aren't the same
1132+ let doc_language_servers = doc.language_servers();
1133+ let spawn_new_servers = language_servers.len() != doc_language_servers.len()
1134+ || language_servers
1135+ .iter()
1136+ .zip(doc_language_servers.iter())
1137+ .any(|(l, dl)| l.id() != dl.id());
1138+ if spawn_new_servers {
1139+ for doc_language_server in doc_language_servers {
1140+ tokio::spawn(doc_language_server.text_document_did_close(doc.identifier()));
11351141 }
11361142
11371143 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
11381144
1139− // TODO: this now races with on_init code if the init happens too quickly
1140− tokio::spawn(language_server.text_document_did_open(
1141− doc_url,
1142− doc.version(),
1143− doc.text(),
1144− language_id,
1145− ));
1145+ for language_server in &language_servers {
1146+ // TODO: this now races with on_init code if the init happens too quickly
1147+ tokio::spawn(language_server.text_document_did_open(
1148+ doc_url.clone(),
1149+ doc.version(),
1150+ doc.text(),
1151+ language_id.clone(),
1152+ ));
1153+ }
11461154
1147− doc.set_language_server(Some(language_server));
1155+ doc.set_language_servers(language_servers);
11481156 }
11491157 }
11501158 Some(())
@@ -1337,10 +1345,10 @@ impl Editor {
13371345 }
13381346 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
13391347
1340− let id = self.new_document(doc);
1341− let _ = self.launch_language_server(id);
1348+ let doc_id = self.new_document(doc);
1349+ let _ = self.launch_language_servers(doc_id);
13421350
1343− id
1351+ doc_id
13441352 };
13451353
13461354 self.switch(id, action);
@@ -1368,7 +1376,7 @@ impl Editor {
13681376 // This will also disallow any follow-up writes
13691377 self.saves.remove(&doc_id);
13701378
1371− if let Some(language_server) = doc.language_server() {
1379+ for language_server in doc.language_servers() {
13721380 // TODO: track error
13731381 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
13741382 }
helix-view/src/gutter.rs+1−1
@@ -55,7 +55,7 @@ pub fn diagnostic<'doc>(
5555 let error = theme.get("error");
5656 let info = theme.get("info");
5757 let hint = theme.get("hint");
58− let diagnostics = doc.diagnostics();
58+ let diagnostics = doc.shown_diagnostics().collect::<Vec<_>>();
5959
6060 Box::new(
6161 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
languages.toml+245−180
@@ -1,6 +1,153 @@
11# Language support configuration.
22# See the languages documentation: https://docs.helix-editor.com/master/languages.html
33
4+[language-server]
5+
6+taplo = { command = "taplo", args = ["lsp", "stdio"] }
7+elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } }
8+mint = { command = "mint", args = ["ls"] }
9+vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } }
10+clangd = { command = "clangd" }
11+crystalline = { command = "crystalline", args = ["--stdio"] }
12+omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
13+vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { "provideFormatter" = true }}
14+vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
15+pylsp = { command = "pylsp" }
16+nls = { command = "nls" }
17+nil = { command = "nil" }
18+solargraph = { command = "solargraph", args = ["stdio"] }
19+bash-language-server = { command = "bash-language-server", args = ["start"] }
20+intelephense = { command = "intelephense", args = ["--stdio"] }
21+texlab = { command = "texlab" }
22+lean = { command = "lean", args = [ "--server" ] }
23+julia = { command = "julia", timeout = 60, args = [
24+ "--startup-file=no",
25+ "--history-file=no",
26+ "--quiet",
27+ "-e",
28+ "using LanguageServer; runserver()",
29+] }
30+jdtls = { command = "jdtls" }
31+ocamllsp = { command = "ocamllsp" }
32+
33+svelteserver = { command = "svelteserver", args = ["--stdio"] }
34+vuels = { command = "vls" }
35+yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
36+haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
37+zls = { command = "zls" }
38+swipl = { command = "swipl", args = [
39+ "-g", "use_module(library(lsp_server))",
40+ "-g", "lsp_server:main",
41+ "-t", "halt", "--", "stdio"
42+] }
43+cmake-language-server = { command = "cmake-language-server" }
44+racket = { command = "racket", args = ["-l", "racket-langserver"] }
45+dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
46+metals = { command = "metals", config = { "isHttpEnabled" = true } }
47+docker-langserver = { command = "docker-langserver", args = ["--stdio"] }
48+elm-language-server = { command = "elm-language-server" }
49+rescript-language-server = { command = "rescript-language-server", args = ["--stdio"] }
50+robotframework_ls = { command = "robotframework_ls" }
51+erlang-ls = { command = "erlang_ls" }
52+kotlin-language-server = { command = "kotlin-language-server" }
53+terraform-ls = { command = "terraform-ls", args = ["serve"] }
54+solc = { command = "solc", args = ["--lsp"] }
55+R = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
56+sourcekit-lsp = { command = "sourcekit-lsp" }
57+vala-language-server = { command = "vala-language-server" }
58+svlangserver = { command = "svlangserver", args = [] }
59+vlang-language-server = { command = "v", args = ["ls"] }
60+openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
61+perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
62+prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
63+clojure-lsp = { command = "clojure-lsp" }
64+wgsl_analyzer = { command = "wgsl_analyzer" }
65+elvish = { command = "elvish", args = ["-lsp"] }
66+idris2-lsp = { command = "idris2-lsp" }
67+fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
68+gleam = { command = "gleam", args = ["lsp"] }
69+ols = { command = "ols", args = [] }
70+dot-language-server = { command = "dot-language-server", args = ["--stdio"] }
71+cuelsp = { command = "cuelsp" }
72+slint-lsp = { command = "slint-lsp", args = [] }
73+awk-language-server = { command = "awk-language-server" }
74+pasls = { command = "pasls", args = [] }
75+jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
76+marksman = { command = "marksman", args = ["server"] }
77+markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
78+bass = { command = "bass", args = ["--lsp"] }
79+purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
80+serve-d = { command = "serve-d" }
81+bicep-langserver = { command = "bicep-langserver" }
82+qmlls = { command = "qmlls" }
83+cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
84+dhall-lsp-server = { command = "dhall-lsp-server" }
85+forc = { command = "forc", args = ["lsp"] }
86+cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
87+vhdl_ls = { command = "vhdl_ls", args = [] }
88+regols = { command = "regols" }
89+nimlangserver = { command = "nimlangserver" }
90+
91+[language-server.rust-analyzer]
92+command = "rust-analyzer"
93+
94+[language-server.rust-analyzer.config]
95+inlayHints.bindingModeHints.enable = false
96+inlayHints.closingBraceHints.minLines = 10
97+inlayHints.closureReturnTypeHints.enable = "with_block"
98+inlayHints.discriminantHints.enable = "fieldless"
99+inlayHints.lifetimeElisionHints.enable = "skip_trivial"
100+inlayHints.typeHints.hideClosureInitialization = false
101+
102+
103+[language-server.lua-language-server]
104+command = "lua-language-server"
105+
106+[language-server.lua-language-server.config.Lua.hint]
107+enable = true
108+arrayIndex = "Enable"
109+setType = true
110+paramName = "All"
111+paramType = true
112+await = true
113+
114+
115+[language-server.gopls]
116+command = "gopls"
117+
118+[language-server.gopls.config.hints]
119+assignVariableTypes = true
120+compositeLiteralFields = true
121+constantValues = true
122+functionTypeParameters = true
123+parameterNames = true
124+rangeVariableTypes = true
125+
126+
127+[language-server.typescript-language-server]
128+command = "typescript-language-server"
129+args = ["--stdio"]
130+config.hostInfo = "helix"
131+
132+[language-server.typescript-language-server.config.typescript.inlayHints]
133+includeInlayEnumMemberValueHints = true
134+includeInlayFunctionLikeReturnTypeHints = true
135+includeInlayFunctionParameterTypeHints = true
136+includeInlayParameterNameHints = "all"
137+includeInlayParameterNameHintsWhenArgumentMatchesName = true
138+includeInlayPropertyDeclarationTypeHints = true
139+includeInlayVariableTypeHints = true
140+
141+[language-server.typescript-language-server.config.javascript.inlayHints]
142+includeInlayEnumMemberValueHints = true
143+includeInlayFunctionLikeReturnTypeHints = true
144+includeInlayFunctionParameterTypeHints = true
145+includeInlayParameterNameHints = "all"
146+includeInlayParameterNameHintsWhenArgumentMatchesName = true
147+includeInlayPropertyDeclarationTypeHints = true
148+includeInlayVariableTypeHints = true
149+
150+
4151[[language]]
5152name = "rust"
6153scope = "source.rust"
@@ -9,7 +156,7 @@ file-types = ["rs"]
9156roots = ["Cargo.toml", "Cargo.lock"]
10157auto-format = true
11158comment-token = "//"
12−language-server = { command = "rust-analyzer" }
159+language-servers = [ "rust-analyzer" ]
13160indent = { tab-width = 4, unit = " " }
14161
15162[language.auto-pairs]
@@ -19,14 +166,6 @@ indent = { tab-width = 4, unit = " " }
19166'"' = '"'
20167'`' = '`'
21168
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−
30169[language.debugger]
31170name = "lldb-vscode"
32171transport = "stdio"
@@ -65,7 +204,7 @@ name = "sway"
65204scope = "source.sway"
66205injection-regex = "sway"
67206file-types = ["sw"]
68−language-server = { command = "forc", args = ["lsp"] }
207+language-servers = [ "forc" ]
69208roots = ["Forc.toml", "Forc.lock"]
70209indent = { tab-width = 4, unit = " " }
71210comment-token = "//"
@@ -81,7 +220,7 @@ injection-regex = "toml"
81220file-types = ["toml", "poetry.lock"]
82221roots = []
83222comment-token = "#"
84−language-server = { command = "taplo", args = ["lsp", "stdio"] }
223+language-servers = [ "taplo" ]
85224indent = { tab-width = 2, unit = " " }
86225
87226[[grammar]]
@@ -95,7 +234,7 @@ injection-regex = "awk"
95234file-types = ["awk", "gawk", "nawk", "mawk"]
96235roots = []
97236comment-token = "#"
98−language-server = { command = "awk-language-server" }
237+language-servers = [ "awk-language-server" ]
99238indent = { tab-width = 2, unit = " " }
100239
101240[[grammar]]
@@ -123,8 +262,7 @@ file-types = ["ex", "exs", "mix.lock"]
123262shebangs = ["elixir"]
124263roots = ["mix.exs", "mix.lock"]
125264comment-token = "#"
126−language-server = { command = "elixir-ls" }
127−config = { elixirLS.dialyzerEnabled = false }
265+language-servers = [ "elixir-ls" ]
128266indent = { tab-width = 2, unit = " " }
129267
130268[[grammar]]
@@ -153,7 +291,7 @@ file-types = ["mint"]
153291shebangs = []
154292roots = []
155293comment-token = "//"
156−language-server = { command = "mint", args = ["ls"] }
294+language-servers = [ "mint" ]
157295indent = { tab-width = 2, unit = " " }
158296
159297[[language]]
@@ -162,9 +300,8 @@ scope = "source.json"
162300injection-regex = "json"
163301file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
164302roots = []
165−language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
303+language-servers = [ "vscode-json-language-server" ]
166304auto-format = true
167−config = { "provideFormatter" = true }
168305indent = { tab-width = 2, unit = " " }
169306
170307[[grammar]]
@@ -178,7 +315,7 @@ injection-regex = "c"
178315file-types = ["c"] # TODO: ["h"]
179316roots = []
180317comment-token = "//"
181−language-server = { command = "clangd" }
318+language-servers = [ "clangd" ]
182319indent = { tab-width = 2, unit = " " }
183320
184321[language.debugger]
@@ -215,7 +352,7 @@ injection-regex = "cpp"
215352file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
216353roots = []
217354comment-token = "//"
218−language-server = { command = "clangd" }
355+language-servers = [ "clangd" ]
219356indent = { tab-width = 2, unit = " " }
220357
221358[language.debugger]
@@ -253,7 +390,7 @@ roots = ["shard.yml", "shard.lock"]
253390comment-token = "#"
254391indent = { tab-width = 2, unit = " " }
255392grammar = "ruby"
256−language-server = { command = "crystalline", args = ["--stdio"] }
393+language-servers = [ "crystalline" ]
257394
258395[[language]]
259396name = "c-sharp"
@@ -263,7 +400,7 @@ file-types = ["cs"]
263400roots = ["sln", "csproj"]
264401comment-token = "//"
265402indent = { tab-width = 4, unit = "\t" }
266−language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
403+language-servers = [ "omnisharp" ]
267404
268405[language.debugger]
269406name = "netcoredbg"
@@ -296,18 +433,10 @@ file-types = ["go"]
296433roots = ["go.work", "go.mod"]
297434auto-format = true
298435comment-token = "//"
299−language-server = { command = "gopls" }
436+language-servers = [ "gopls" ]
300437# TODO: gopls needs utf-8 offsets?
301438indent = { tab-width = 4, unit = "\t" }
302439
303−[language.config.hints]
304−assignVariableTypes = true
305−compositeLiteralFields = true
306−constantValues = true
307−functionTypeParameters = true
308−parameterNames = true
309−rangeVariableTypes = true
310−
311440[language.debugger]
312441name = "go"
313442transport = "tcp"
@@ -351,7 +480,7 @@ file-types = ["go.mod"]
351480roots = []
352481auto-format = true
353482comment-token = "//"
354−language-server = { command = "gopls" }
483+language-servers = [ "gopls" ]
355484indent = { tab-width = 4, unit = "\t" }
356485
357486[[grammar]]
@@ -365,7 +494,7 @@ injection-regex = "gotmpl"
365494file-types = ["gotmpl"]
366495roots = []
367496comment-token = "//"
368−language-server = { command = "gopls" }
497+language-servers = [ "gopls" ]
369498indent = { tab-width = 2, unit = " " }
370499
371500[[grammar]]
@@ -380,7 +509,7 @@ file-types = ["go.work"]
380509roots = []
381510auto-format = true
382511comment-token = "//"
383−language-server = { command = "gopls" }
512+language-servers = [ "gopls" ]
384513indent = { tab-width = 4, unit = "\t" }
385514
386515[[grammar]]
@@ -391,26 +520,15 @@ source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd
391520name = "javascript"
392521scope = "source.js"
393522injection-regex = "(js|javascript)"
523+language-id = "javascript"
394524file-types = ["js", "mjs", "cjs"]
395525shebangs = ["node"]
396526roots = []
397527comment-token = "//"
398528# TODO: highlights-params
399−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
529+language-servers = [ "typescript-language-server" ]
400530indent = { tab-width = 2, unit = " " }
401531
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−
414532[language.debugger]
415533name = "node-debug2"
416534transport = "stdio"
@@ -431,48 +549,26 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev =
431549name = "jsx"
432550scope = "source.jsx"
433551injection-regex = "jsx"
552+language-id = "javascriptreact"
434553file-types = ["jsx"]
435554roots = []
436555comment-token = "//"
437−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascriptreact" }
556+language-servers = [ "typescript-language-server" ]
438557indent = { tab-width = 2, unit = " " }
439558grammar = "javascript"
440559
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−
453560[[language]]
454561name = "typescript"
455562scope = "source.ts"
456563injection-regex = "(ts|typescript)"
457564file-types = ["ts", "mts", "cts"]
565+language-id = "typescript"
458566shebangs = []
459567roots = []
460568# TODO: highlights-params
461−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
569+language-servers = [ "typescript-language-server" ]
462570indent = { tab-width = 2, unit = " " }
463571
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−
476572[[grammar]]
477573name = "typescript"
478574source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
@@ -481,24 +577,13 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev =
481577name = "tsx"
482578scope = "source.tsx"
483579injection-regex = "(tsx)" # |typescript
580+language-id = "typescriptreact"
484581file-types = ["tsx"]
485582roots = []
486583# TODO: highlights-params
487−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
584+language-servers = [ "typescript-language-server" ]
488585indent = { tab-width = 2, unit = " " }
489586
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−
502587[[grammar]]
503588name = "tsx"
504589source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@@ -509,9 +594,8 @@ scope = "source.css"
509594injection-regex = "css"
510595file-types = ["css", "scss"]
511596roots = []
512−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
597+language-servers = [ "vscode-css-language-server" ]
513598auto-format = true
514−config = { "provideFormatter" = true }
515599indent = { tab-width = 2, unit = " " }
516600
517601[[grammar]]
@@ -524,9 +608,8 @@ scope = "source.scss"
524608injection-regex = "scss"
525609file-types = ["scss"]
526610roots = []
527−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
611+language-servers = [ "vscode-css-language-server" ]
528612auto-format = true
529−config = { "provideFormatter" = true }
530613indent = { tab-width = 2, unit = " " }
531614
532615[[grammar]]
@@ -539,9 +622,8 @@ scope = "text.html.basic"
539622injection-regex = "html"
540623file-types = ["html"]
541624roots = []
542−language-server = { command = "vscode-html-language-server", args = ["--stdio"] }
625+language-servers = [ "vscode-html-language-server" ]
543626auto-format = true
544−config = { "provideFormatter" = true }
545627indent = { tab-width = 2, unit = " " }
546628
547629[[grammar]]
@@ -556,7 +638,7 @@ file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstr
556638shebangs = ["python"]
557639roots = []
558640comment-token = "#"
559−language-server = { command = "pylsp" }
641+language-servers = [ "pylsp" ]
560642# TODO: pyls needs utf-8 offsets
561643indent = { tab-width = 4, unit = " " }
562644
@@ -572,7 +654,7 @@ file-types = ["ncl"]
572654shebangs = []
573655roots = []
574656comment-token = "#"
575−language-server = { command = "nls" }
657+language-servers = [ "nls" ]
576658indent = { tab-width = 2, unit = " " }
577659
578660[[grammar]]
@@ -587,7 +669,7 @@ file-types = ["nix"]
587669shebangs = []
588670roots = []
589671comment-token = "#"
590−language-server = { command = "nil" }
672+language-servers = [ "nil" ]
591673indent = { tab-width = 2, unit = " " }
592674
593675[[grammar]]
@@ -602,7 +684,7 @@ file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile",
602684shebangs = ["ruby"]
603685roots = []
604686comment-token = "#"
605−language-server = { command = "solargraph", args = ["stdio"] }
687+language-servers = [ "solargraph" ]
606688indent = { tab-width = 2, unit = " " }
607689
608690[[grammar]]
@@ -617,7 +699,7 @@ file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile
617699shebangs = ["sh", "bash", "dash", "zsh"]
618700roots = []
619701comment-token = "#"
620−language-server = { command = "bash-language-server", args = ["start"] }
702+language-servers = [ "bash-language-server" ]
621703indent = { tab-width = 2, unit = " " }
622704
623705[[grammar]]
@@ -631,7 +713,7 @@ injection-regex = "php"
631713file-types = ["php", "inc"]
632714shebangs = ["php"]
633715roots = ["composer.json", "index.php"]
634−language-server = { command = "intelephense", args = ["--stdio"] }
716+language-servers = [ "intelephense" ]
635717indent = { tab-width = 4, unit = " " }
636718
637719[[grammar]]
@@ -657,7 +739,7 @@ injection-regex = "tex"
657739file-types = ["tex", "sty", "cls"]
658740roots = []
659741comment-token = "%"
660−language-server = { command = "texlab" }
742+language-servers = [ "texlab" ]
661743indent = { tab-width = 4, unit = "\t" }
662744
663745[[grammar]]
@@ -671,7 +753,7 @@ injection-regex = "bib"
671753file-types = ["bib"]
672754roots = []
673755comment-token = "%"
674−language-server = { command = "texlab" }
756+language-servers = [ "texlab" ]
675757indent = { tab-width = 4, unit = "\t" }
676758auto-format = true
677759
@@ -699,7 +781,7 @@ injection-regex = "lean"
699781file-types = ["lean"]
700782roots = [ "lakefile.lean" ]
701783comment-token = "--"
702−language-server = { command = "lean", args = [ "--server" ] }
784+language-servers = [ "lean" ]
703785indent = { tab-width = 2, unit = " " }
704786
705787[[grammar]]
@@ -713,13 +795,7 @@ injection-regex = "julia"
713795file-types = ["jl"]
714796roots = ["Manifest.toml", "Project.toml"]
715797comment-token = "#"
716−language-server = { command = "julia", timeout = 60, args = [
717− "--startup-file=no",
718− "--history-file=no",
719− "--quiet",
720− "-e",
721− "using LanguageServer; runserver()",
722− ] }
798+language-servers = [ "julia" ]
723799indent = { tab-width = 4, unit = " " }
724800
725801[[grammar]]
@@ -732,7 +808,7 @@ scope = "source.java"
732808injection-regex = "java"
733809file-types = ["java"]
734810roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
735−language-server = { command = "jdtls" }
811+language-servers = [ "jdtls" ]
736812indent = { tab-width = 4, unit = " " }
737813
738814[[grammar]]
@@ -773,7 +849,7 @@ file-types = ["ml"]
773849shebangs = []
774850roots = []
775851comment-token = "(**)"
776−language-server = { command = "ocamllsp" }
852+language-servers = [ "ocamllsp" ]
777853indent = { tab-width = 2, unit = " " }
778854
779855[language.auto-pairs]
@@ -794,7 +870,7 @@ file-types = ["mli"]
794870shebangs = []
795871roots = []
796872comment-token = "(**)"
797−language-server = { command = "ocamllsp" }
873+language-servers = [ "ocamllsp" ]
798874indent = { tab-width = 2, unit = " " }
799875
800876[language.auto-pairs]
@@ -817,15 +893,7 @@ shebangs = ["lua"]
817893roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
818894comment-token = "--"
819895indent = { tab-width = 2, unit = " " }
820−language-server = { command = "lua-language-server", args = [] }
821−
822−[language.config.Lua.hint]
823−enable = true
824−arrayIndex = "Enable"
825−setType = true
826−paramName = "All"
827−paramType = true
828−await = true
896+language-servers = [ "lua-language-server" ]
829897
830898[[grammar]]
831899name = "lua"
@@ -838,7 +906,7 @@ injection-regex = "svelte"
838906file-types = ["svelte"]
839907roots = []
840908indent = { tab-width = 2, unit = " " }
841−language-server = { command = "svelteserver", args = ["--stdio"] }
909+language-servers = [ "svelteserver" ]
842910
843911[[grammar]]
844912name = "svelte"
@@ -851,7 +919,7 @@ injection-regex = "vue"
851919file-types = ["vue"]
852920roots = ["package.json", "vue.config.js"]
853921indent = { tab-width = 2, unit = " " }
854−language-server = { command = "vls" }
922+language-servers = [ "vuels" ]
855923
856924[[grammar]]
857925name = "vue"
@@ -864,7 +932,7 @@ file-types = ["yml", "yaml"]
864932roots = []
865933comment-token = "#"
866934indent = { tab-width = 2, unit = " " }
867−language-server = { command = "yaml-language-server", args = ["--stdio"] }
935+language-servers = [ "yaml-language-server" ]
868936injection-regex = "yml|yaml"
869937
870938[[grammar]]
@@ -878,7 +946,7 @@ injection-regex = "haskell"
878946file-types = ["hs", "hs-boot"]
879947roots = ["Setup.hs", "stack.yaml", "cabal.project"]
880948comment-token = "--"
881−language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
949+language-servers = [ "haskell-language-server" ]
882950indent = { tab-width = 2, unit = " " }
883951
884952[[grammar]]
@@ -892,7 +960,7 @@ injection-regex = "purescript"
892960file-types = ["purs"]
893961roots = ["spago.dhall", "bower.json"]
894962comment-token = "--"
895−language-server = { command = "purescript-language-server", args = ["--stdio"] }
963+language-servers = [ "purescript-language-server" ]
896964indent = { tab-width = 2, unit = " " }
897965auto-format = true
898966formatter = { command = "purs-tidy", args = ["format"] }
@@ -906,7 +974,7 @@ file-types = ["zig"]
906974roots = ["build.zig"]
907975auto-format = true
908976comment-token = "//"
909−language-server = { command = "zls" }
977+language-servers = [ "zls" ]
910978indent = { tab-width = 4, unit = " " }
911979formatter = { command = "zig" , args = ["fmt", "--stdin"] }
912980
@@ -944,10 +1012,7 @@ roots = []
9441012file-types = ["pl", "prolog"]
9451013shebangs = ["swipl"]
9461014comment-token = "%"
947−language-server = { command = "swipl", args = [
948− "-g", "use_module(library(lsp_server))",
949− "-g", "lsp_server:main",
950− "-t", "halt", "--", "stdio"] }
1015+language-servers = [ "swipl" ]
9511016
9521017[[language]]
9531018name = "tsq"
@@ -969,7 +1034,7 @@ file-types = ["cmake", "CMakeLists.txt"]
9691034roots = []
9701035comment-token = "#"
9711036indent = { tab-width = 2, unit = " " }
972−language-server = { command = "cmake-language-server" }
1037+language-servers = [ "cmake-language-server" ]
9731038injection-regex = "cmake"
9741039
9751040[[grammar]]
@@ -1009,7 +1074,7 @@ file-types = ["pl", "pm", "t"]
10091074shebangs = ["perl"]
10101075roots = []
10111076comment-token = "#"
1012−language-server = { command = "perlnavigator", args= ["--stdio"] }
1077+language-servers = [ "perlnavigator" ]
10131078indent = { tab-width = 2, unit = " " }
10141079
10151080[[grammar]]
@@ -1023,7 +1088,7 @@ roots = []
10231088file-types = ["rkt", "rktd", "rktl", "scrbl"]
10241089shebangs = ["racket"]
10251090comment-token = ";"
1026−language-server = { command = "racket", args = ["-l", "racket-langserver"] }
1091+language-servers = [ "racket" ]
10271092grammar = "scheme"
10281093
10291094[[language]]
@@ -1034,7 +1099,7 @@ file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
10341099shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
10351100comment-token = ";"
10361101indent = { tab-width = 2, unit = " " }
1037−language-server = { command = "cl-lsp", args = [ "stdio" ] }
1102+language-servers = [ "cl-lsp" ]
10381103grammar = "scheme"
10391104
10401105[language.auto-pairs]
@@ -1060,7 +1125,7 @@ scope = "source.wgsl"
10601125file-types = ["wgsl"]
10611126roots = []
10621127comment-token = "//"
1063−language-server = { command = "wgsl_analyzer" }
1128+language-servers = [ "wgsl_analyzer" ]
10641129indent = { tab-width = 4, unit = " " }
10651130
10661131[[grammar]]
@@ -1123,7 +1188,7 @@ scope = "source.md"
11231188injection-regex = "md|markdown"
11241189file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
11251190roots = [".marksman.toml"]
1126−language-server = { command = "marksman", args=["server"] }
1191+language-servers = [ "marksman" ]
11271192indent = { tab-width = 2, unit = " " }
11281193
11291194[[grammar]]
@@ -1149,7 +1214,7 @@ file-types = ["dart"]
11491214roots = ["pubspec.yaml"]
11501215auto-format = true
11511216comment-token = "//"
1152−language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
1217+language-servers = [ "dart" ]
11531218indent = { tab-width = 2, unit = " " }
11541219
11551220[[grammar]]
@@ -1163,8 +1228,7 @@ roots = ["build.sbt", "build.sc", "build.gradle", "build.gradle.kts", "pom.xml",
11631228file-types = ["scala", "sbt", "sc"]
11641229comment-token = "//"
11651230indent = { tab-width = 2, unit = " " }
1166−language-server = { command = "metals" }
1167−config = { "isHttpEnabled" = true }
1231+language-servers = [ "metals" ]
11681232
11691233[[grammar]]
11701234name = "scala"
@@ -1178,7 +1242,7 @@ roots = ["Dockerfile", "Containerfile"]
11781242file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
11791243comment-token = "#"
11801244indent = { tab-width = 2, unit = " " }
1181−language-server = { command = "docker-langserver", args = ["--stdio"] }
1245+language-servers = [ "docker-langserver" ]
11821246
11831247[[grammar]]
11841248name = "dockerfile"
@@ -1218,7 +1282,7 @@ roots = []
12181282file-types = ["git-rebase-todo"]
12191283injection-regex = "git-rebase"
12201284comment-token = "#"
1221−indent = { tab-width = 2, unit = " " }
1285+indent = { tab-width = 2, unit = "y" }
12221286
12231287[[grammar]]
12241288name = "git-rebase"
@@ -1294,7 +1358,7 @@ file-types = ["elm"]
12941358roots = ["elm.json"]
12951359auto-format = true
12961360comment-token = "--"
1297−language-server = { command = "elm-language-server" }
1361+language-servers = [ "elm-language-server" ]
12981362indent = { tab-width = 4, unit = " " }
12991363
13001364[[grammar]]
@@ -1320,7 +1384,7 @@ file-types = ["res"]
13201384roots = ["bsconfig.json"]
13211385auto-format = true
13221386comment-token = "//"
1323−language-server = { command = "rescript-language-server", args = ["--stdio"] }
1387+language-servers = [ "rescript-language-server" ]
13241388indent = { tab-width = 2, unit = " " }
13251389
13261390[[grammar]]
@@ -1336,7 +1400,7 @@ roots = ["rebar.config"]
13361400shebangs = ["escript"]
13371401comment-token = "%%"
13381402indent = { tab-width = 4, unit = " " }
1339−language-server = { command = "erlang_ls" }
1403+language-servers = [ "erlang-ls" ]
13401404
13411405[language.auto-pairs]
13421406'(' = ')'
@@ -1357,7 +1421,7 @@ file-types = ["kt", "kts"]
13571421roots = ["settings.gradle", "settings.gradle.kts"]
13581422comment-token = "//"
13591423indent = { tab-width = 4, unit = " " }
1360−language-server = { command = "kotlin-language-server" }
1424+language-servers = [ "kotlin-language-server" ]
13611425
13621426[[grammar]]
13631427name = "kotlin"
@@ -1367,11 +1431,12 @@ source = { git = "https://github.com/fwcd/tree-sitter-kotlin", rev = "a4f71eb9b8
13671431name = "hcl"
13681432scope = "source.hcl"
13691433injection-regex = "(hcl|tf|nomad)"
1434+language-id = "terraform"
13701435file-types = ["hcl", "tf", "nomad"]
13711436roots = []
13721437comment-token = "#"
13731438indent = { tab-width = 2, unit = " " }
1374−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform" }
1439+language-servers = [ "terraform-ls" ]
13751440auto-format = true
13761441
13771442[[grammar]]
@@ -1381,11 +1446,12 @@ source = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "3cb7
13811446[[language]]
13821447name = "tfvars"
13831448scope = "source.tfvars"
1449+language-id = "terraform-vars"
13841450file-types = ["tfvars"]
13851451roots = []
13861452comment-token = "#"
13871453indent = { tab-width = 2, unit = " " }
1388−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform-vars" }
1454+language-servers = [ "terraform-ls" ]
13891455auto-format = true
13901456grammar = "hcl"
13911457
@@ -1409,7 +1475,7 @@ file-types = ["sol"]
14091475roots = []
14101476comment-token = "//"
14111477indent = { tab-width = 4, unit = " " }
1412−language-server = { command = "solc", args = ["--lsp"] }
1478+language-servers = [ "solc" ]
14131479
14141480[[grammar]]
14151481name = "solidity"
@@ -1423,7 +1489,7 @@ file-types = ["gleam"]
14231489roots = ["gleam.toml"]
14241490comment-token = "//"
14251491indent = { tab-width = 2, unit = " " }
1426−language-server = { command = "gleam", args = ["lsp"] }
1492+language-servers = [ "gleam" ]
14271493
14281494[[grammar]]
14291495name = "gleam"
@@ -1447,7 +1513,7 @@ file-types = ["robot", "resource"]
14471513comment-token = "#"
14481514roots = []
14491515indent = { tab-width = 4, unit = " " }
1450−language-server = { command = "robotframework_ls" }
1516+language-servers = [ "robotframework_ls" ]
14511517
14521518[[grammar]]
14531519name = "robot"
@@ -1462,7 +1528,7 @@ shebangs = ["r", "R"]
14621528roots = []
14631529comment-token = "#"
14641530indent = { tab-width = 2, unit = " " }
1465−language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
1531+language-servers = [ "R" ]
14661532
14671533[[grammar]]
14681534name = "r"
@@ -1476,7 +1542,7 @@ file-types = ["rmd", "Rmd"]
14761542roots = []
14771543indent = { tab-width = 2, unit = " " }
14781544grammar = "markdown"
1479−language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
1545+language-servers = [ "R" ]
14801546
14811547[[language]]
14821548name = "swift"
@@ -1486,7 +1552,7 @@ file-types = ["swift"]
14861552roots = [ "Package.swift" ]
14871553comment-token = "//"
14881554auto-format = true
1489−language-server = { command = "sourcekit-lsp" }
1555+language-servers = [ "sourcekit-lsp" ]
14901556
14911557[[grammar]]
14921558name = "swift"
@@ -1533,8 +1599,7 @@ injection-regex = "heex"
15331599file-types = ["heex"]
15341600roots = ["mix.exs", "mix.lock"]
15351601indent = { tab-width = 2, unit = " " }
1536−language-server = { command = "elixir-ls" }
1537−config = { elixirLS.dialyzerEnabled = false }
1602+language-servers = [ "elixir-ls" ]
15381603
15391604[[grammar]]
15401605name = "heex"
@@ -1605,7 +1670,7 @@ file-types = ["vala", "vapi"]
16051670roots = []
16061671comment-token = "//"
16071672indent = { tab-width = 2, unit = " " }
1608−language-server = { command = "vala-language-server" }
1673+language-servers = [ "vala-language-server" ]
16091674
16101675[[grammar]]
16111676name = "vala"
@@ -1670,7 +1735,7 @@ auto-format = false
16701735scope = "source.odin"
16711736file-types = ["odin"]
16721737roots = ["ols.json"]
1673−language-server = { command = "ols", args = [] }
1738+language-servers = [ "ols" ]
16741739comment-token = "//"
16751740indent = { tab-width = 4, unit = "\t" }
16761741
@@ -1721,7 +1786,7 @@ scope = "source.v"
17211786file-types = ["v", "vv", "vsh"]
17221787shebangs = ["v run"]
17231788roots = ["v.mod"]
1724−language-server = { command = "v", args = ["ls"] }
1789+language-servers = [ "vlang-language-server" ]
17251790auto-format = true
17261791comment-token = "//"
17271792indent = { tab-width = 4, unit = "\t" }
@@ -1736,7 +1801,7 @@ scope = "source.verilog"
17361801file-types = ["v", "vh", "sv", "svh"]
17371802roots = []
17381803comment-token = "//"
1739−language-server = { command = "svlangserver", args = [] }
1804+language-servers = [ "svlangserver" ]
17401805indent = { tab-width = 2, unit = " " }
17411806injection-regex = "verilog"
17421807
@@ -1775,7 +1840,7 @@ injection-regex = "openscad"
17751840file-types = ["scad"]
17761841roots = []
17771842comment-token = "//"
1778−language-server = { command = "openscad-lsp", args = ["--stdio"] }
1843+language-servers = [ "openscad-lsp" ]
17791844indent = { tab-width = 2, unit = "\t" }
17801845
17811846[[grammar]]
@@ -1789,7 +1854,7 @@ injection-regex = "prisma"
17891854file-types = ["prisma"]
17901855roots = ["package.json"]
17911856comment-token = "//"
1792−language-server = { command = "prisma-language-server", args = ["--stdio"] }
1857+language-servers = [ "prisma-language-server" ]
17931858indent = { tab-width = 2, unit = " " }
17941859
17951860[[grammar]]
@@ -1803,7 +1868,7 @@ injection-regex = "(clojure|clj|edn|boot)"
18031868file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
18041869roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
18051870comment-token = ";"
1806−language-server = { command = "clojure-lsp" }
1871+language-servers = [ "clojure-lsp" ]
18071872indent = { tab-width = 2, unit = " " }
18081873
18091874[[grammar]]
@@ -1827,7 +1892,7 @@ file-types = ["elv"]
18271892roots = []
18281893comment-token = "#"
18291894indent = { tab-width = 2, unit = " " }
1830−language-server = { command = "elvish", args = ["-lsp"] }
1895+language-servers = [ "elvish" ]
18311896grammar = "elvish"
18321897
18331898[[grammar]]
@@ -1843,7 +1908,7 @@ shebangs = []
18431908roots = []
18441909comment-token = "--"
18451910indent = { tab-width = 2, unit = " " }
1846−language-server = { command = "idris2-lsp" }
1911+language-servers = [ "idris2-lsp" ]
18471912
18481913[[language]]
18491914name = "fortran"
@@ -1853,7 +1918,7 @@ file-types = ["f", "for", "f90", "f95", "f03"]
18531918roots = ["fpm.toml"]
18541919comment-token = "!"
18551920indent = { tab-width = 4, unit = " "}
1856−language-server = { command = "fortls", args = ["--lowercase_intrinsics"] }
1921+language-servers = [ "fortls" ]
18571922
18581923[[grammar]]
18591924name = "fortran"
@@ -1880,7 +1945,7 @@ file-types = ["dot"]
18801945roots = []
18811946comment-token = "//"
18821947indent = { tab-width = 4, unit = " " }
1883−language-server = { command = "dot-language-server", args = ["--stdio"] }
1948+language-servers = [ "dot-language-server" ]
18841949
18851950[[grammar]]
18861951name = "dot"
@@ -1894,7 +1959,7 @@ file-types = ["cue"]
18941959roots = ["cue.mod"]
18951960auto-format = true
18961961comment-token = "//"
1897−language-server = { command = "cuelsp" }
1962+language-servers = [ "cuelsp" ]
18981963indent = { tab-width = 4, unit = "\t" }
18991964formatter = { command = "cue", args = ["fmt", "-"] }
19001965
@@ -1910,7 +1975,7 @@ file-types = ["slint"]
19101975roots = []
19111976comment-token = "//"
19121977indent = { tab-width = 4, unit = " " }
1913−language-server = { command = "slint-lsp", args = [] }
1978+language-servers = [ "slint-lsp" ]
19141979
19151980[[grammar]]
19161981name = "slint"
@@ -1962,7 +2027,7 @@ file-types = ["pas", "pp", "inc", "lpr", "lfm"]
19622027roots = []
19632028comment-token = "//"
19642029indent = { tab-width = 2, unit = " " }
1965−language-server = { command = "pasls", args = [] }
2030+language-servers = [ "pasls" ]
19662031
19672032[[grammar]]
19682033name = "pascal"
@@ -1987,7 +2052,7 @@ file-types = ["libsonnet", "jsonnet"]
19872052roots = ["jsonnetfile.json"]
19882053comment-token = "//"
19892054indent = { tab-width = 2, unit = " " }
1990−language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
2055+language-servers = [ "jsonnet-language-server" ]
19912056
19922057[[grammar]]
19932058name = "jsonnet"
@@ -2013,7 +2078,7 @@ file-types = ["bass"]
20132078roots = []
20142079comment-token = ";"
20152080indent = { tab-width = 2, unit = " " }
2016−language-server = { command = "bass", args = ["--lsp"] }
2081+language-servers = [ "bass" ]
20172082
20182083[[grammar]]
20192084name = "bass"
@@ -2049,7 +2114,7 @@ roots = []
20492114comment-token = "//"
20502115injection-regex = "d"
20512116indent = { tab-width = 4, unit = " "}
2052−language-server = { command = "serve-d" }
2117+language-servers = [ "serve-d" ]
20532118formatter = { command = "dfmt" }
20542119
20552120[[grammar]]
@@ -2173,7 +2238,7 @@ roots = []
21732238auto-format = true
21742239comment-token = "//"
21752240indent = { tab-width = 2, unit = " "}
2176−language-server = { command = "bicep-langserver" }
2241+language-servers = [ "bicep-langserver" ]
21772242
21782243[[grammar]]
21792244name = "bicep"
@@ -2184,7 +2249,7 @@ name = "qml"
21842249scope = "source.qml"
21852250file-types = ["qml"]
21862251roots = []
2187−language-server = { command = "qmlls" }
2252+language-servers = [ "qmlls" ]
21882253indent = { tab-width = 4, unit = " " }
21892254grammar = "qmljs"
21902255
@@ -2239,7 +2304,7 @@ file-types = ["dhall"]
22392304roots = []
22402305comment-token = "--"
22412306indent = { tab-width = 2, unit = " " }
2242−language-server = { command = "dhall-lsp-server" }
2307+language-servers = [ "dhall-lsp-server" ]
22432308formatter = { command = "dhall" , args = ["format"] }
22442309
22452310[[grammar]]
@@ -2401,7 +2466,7 @@ file-types = ["smithy"]
24012466roots = ["smithy-build.json"]
24022467comment-token = "//"
24032468indent = { tab-width = 4, unit = " " }
2404−language-server = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
2469+language-servers = [ "cs" ]
24052470
24062471[[grammar]]
24072472name = "smithy"
@@ -2413,7 +2478,7 @@ scope = "source.vhdl"
24132478file-types = ["vhd", "vhdl"]
24142479roots = []
24152480comment-token = "--"
2416−language-server = { command = "vhdl_ls", args = [] }
2481+language-servers = [ "vhdl_ls" ]
24172482indent = { tab-width = 2, unit = " " }
24182483injection-regex = "vhdl"
24192484
@@ -2429,7 +2494,7 @@ injection-regex = "rego"
24292494file-types = ["rego"]
24302495auto-format = true
24312496comment-token = "#"
2432−language-server = { command = "regols" }
2497+language-servers = [ "regols" ]
24332498grammar = "rego"
24342499
24352500[[grammar]]
@@ -2445,7 +2510,7 @@ shebangs = []
24452510roots = []
24462511comment-token = "#"
24472512indent = { tab-width = 2, unit = " " }
2448−language-server = { command = "nimlangserver" }
2513+language-servers = [ "nimlangserver" ]
24492514
24502515[language.auto-pairs]
24512516'(' = ')'
@@ -2485,7 +2550,7 @@ name = "markdoc"
24852550scope = "text.markdoc"
24862551roots = []
24872552file-types = ["mdoc"]
2488−language-server = { command = "markdoc-ls", args = ["--stdio"] }
2553+language-servers = [ "markdoc-ls" ]
24892554
24902555[[grammar]]
24912556name = "markdoc"
@@ -2498,7 +2563,7 @@ injection-regex = "(cl|opencl)"
24982563file-types = ["cl"]
24992564roots = []
25002565comment-token = "//"
2501−language-server = { command = "clangd" }
2566+language-servers = [ "clangd" ]
25022567
25032568[[grammar]]
25042569name = "opencl"
xtask/src/docgen.rs+6−5
@@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
9696 );
9797 }
9898 row.push(
99− lc.language_server
100− .as_ref()
101− .map(|s| s.command.clone())
102− .map(|c| md_mono(&c))
103− .unwrap_or_default(),
99+ lc.language_servers
100+ .iter()
101+ .filter_map(|ls| config.language_server.get(ls.name()))
102+ .map(|s| md_mono(&s.command.clone()))
103+ .collect::<Vec<_>>()
104+ .join(", "),
104105 );
105106
106107 md.push_str(&md_table_row(&row));