on master help sign in
Adds support for multiple language servers per language.
71551d3Philipp Mildenberger
book/src/generated/typable-cmd.md+1−1
@@ -50,7 +50,7 @@
50| `:reload-all` | Discard changes and reload all documents from the source files. |
50| `:reload-all` | Discard changes and reload all documents from the source files. |
51| `:update`, `:u` | Write changes only if the file has been modified. |
51| `:update`, `:u` | Write changes only if the file has been modified. |
52| `:lsp-workspace-command` | Open workspace command picker |
52| `:lsp-workspace-command` | Open workspace command picker |
53−| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
53+| `:lsp-restart` | Restarts the language servers used by the currently opened file |
54| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
54| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
55| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
55| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
56| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
56| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
book/src/guides/adding_languages.md+1
@@ -9,6 +9,7 @@ below.
9 necessary configuration for the new language. For more information on
9 necessary configuration for the new language. For more information on
10 language configuration, refer to the
10 language configuration, refer to the
11 [language configuration section](../languages.md) of the documentation.
11 [language configuration section](../languages.md) of the documentation.
12+ A new language server can be added by extending the `[language-server]` table in the same file.
122. If you are adding a new language or updating an existing language server
132. If you are adding a new language or updating an existing language server
13 configuration, run the command `cargo xtask docgen` to update the
14 configuration, run the command `cargo xtask docgen` to update the
14 [Language Support](../lang-support.md) documentation.
15 [Language Support](../lang-support.md) documentation.
book/src/languages.md+87−17
@@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
18```toml
18```toml
19# in <config_dir>/helix/languages.toml
19# in <config_dir>/helix/languages.toml
20
20
21+[language-server.mylang-lsp]
22+command = "mylang-lsp"
23+
21[[language]]
24[[language]]
22name = "rust"
25name = "rust"
23auto-format = false
26auto-format = false
@@ -41,8 +44,8 @@ injection-regex = "mylang"
41file-types = ["mylang", "myl"]
44file-types = ["mylang", "myl"]
42comment-token = "#"
45comment-token = "#"
43indent = { tab-width = 2, unit = " " }
46indent = { tab-width = 2, unit = " " }
44−language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
45formatter = { command = "mylang-formatter" , args = ["--stdin"] }
47formatter = { command = "mylang-formatter" , args = ["--stdin"] }
48+language-servers = [ "mylang-lsp" ]
46```
49```
47
50
48These configuration keys are available:
51These configuration keys are available:
@@ -50,6 +53,7 @@ These configuration keys are available:
50| Key | Description |
53| Key | Description |
51| ---- | ----------- |
54| ---- | ----------- |
52| `name` | The name of the language |
55| `name` | The name of the language |
56+| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
53| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
57| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
54| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
58| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
55| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
59| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@@ -59,7 +63,7 @@ These configuration keys are available:
59| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
63| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
60| `comment-token` | The token to use as a comment-token |
64| `comment-token` | The token to use as a comment-token |
61| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
65| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
62−| `language-server` | The Language Server to run. See the Language Server configuration section below. |
66+| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
63| `config` | Language Server configuration |
67| `config` | Language Server configuration |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
68| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
65| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
69| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
@@ -92,31 +96,97 @@ with the following priorities:
92 replaced at runtime with the appropriate path separator for the operating
96 replaced at runtime with the appropriate path separator for the operating
93 system, so this rule would match against `.git\config` files on Windows.
97 system, so this rule would match against `.git\config` files on Windows.
94
98
95−### Language Server configuration
99+## Language Server configuration
100+
101+Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
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+```
96
119
97−The `language-server` field takes the following keys:
120+These are the available options for a language server.
98
121
99−| Key | Description |
122+| Key | Description |
100−| --- | ----------- |
123+| ---- | ----------- |
101−| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
124+| `command` | The name or path 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 |
125+| `args` | A list of arguments to pass to the language server binary |
126+| `config` | LSP initialization options |
103−| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
127+| `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" }` |
128+| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
106
129
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
130+A `format` sub-table within `config` can be used to pass extra formatting options to
109−[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook).
131+[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
110For example with typescript:
132For example with typescript:
111
133
112```toml
134```toml
113−[[language]]
135+[language-server.typescript-language-server]
114−name = "typescript"
115−auto-format = true
116# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
136# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
117config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
137config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
118```
138```
119
139
140+### Configuring Language Servers for a language
141+
142+The `language-servers` attribute in a language tells helix which language servers are used for this language.
143+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+
120## Tree-sitter grammar configuration
190## Tree-sitter grammar configuration
121
191
122The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
192The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
helix-core/src/diagnostic.rs+1
@@ -43,6 +43,7 @@ pub struct Diagnostic {
43 pub message: String,
43 pub message: String,
44 pub severity: Option<Severity>,
44 pub severity: Option<Severity>,
45 pub code: Option<NumberOrString>,
45 pub code: Option<NumberOrString>,
46+ pub language_server_id: usize,
46 pub tags: Vec<DiagnosticTag>,
47 pub tags: Vec<DiagnosticTag>,
47 pub source: Option<String>,
48 pub source: Option<String>,
48 pub data: Option<serde_json::Value>,
49 pub data: Option<serde_json::Value>,
helix-core/src/syntax.rs+102−11
@@ -17,7 +17,7 @@ use std::{
17 borrow::Cow,
17 borrow::Cow,
18 cell::RefCell,
18 cell::RefCell,
19 collections::{HashMap, VecDeque},
19 collections::{HashMap, VecDeque},
20− fmt,
20+ fmt::{self, Display},
21 hash::{Hash, Hasher},
21 hash::{Hash, Hasher},
22 mem::{replace, transmute},
22 mem::{replace, transmute},
23 path::{Path, PathBuf},
23 path::{Path, PathBuf},
@@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
60}
60}
61
61
62#[derive(Debug, Serialize, Deserialize)]
62#[derive(Debug, Serialize, Deserialize)]
63+#[serde(rename_all = "kebab-case")]
63pub struct Configuration {
64pub struct Configuration {
64 pub language: Vec<LanguageConfiguration>,
65 pub language: Vec<LanguageConfiguration>,
66+ #[serde(default)]
67+ pub language_server: HashMap<String, LanguageServerConfiguration>,
65}
68}
66
69
67impl Default for Configuration {
70impl Default for Configuration {
@@ -75,7 +78,10 @@ impl Default for Configuration {
75#[serde(rename_all = "kebab-case", deny_unknown_fields)]
78#[serde(rename_all = "kebab-case", deny_unknown_fields)]
76pub struct LanguageConfiguration {
79pub struct LanguageConfiguration {
77 #[serde(rename = "name")]
80 #[serde(rename = "name")]
78− pub language_id: String, // c-sharp, rust
81+ pub language_id: String, // c-sharp, rust, tsx
82+ #[serde(rename = "language-id")]
83+ // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
84+ pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
79 pub scope: String, // source.rust
85 pub scope: String, // source.rust
80 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
86 pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
81 #[serde(default)]
87 #[serde(default)]
@@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
85 pub text_width: Option<usize>,
91 pub text_width: Option<usize>,
86 pub soft_wrap: Option<SoftWrap>,
92 pub soft_wrap: Option<SoftWrap>,
87
93
88− #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
89− pub config: Option<serde_json::Value>,
90−
91 #[serde(default)]
94 #[serde(default)]
92 pub auto_format: bool,
95 pub auto_format: bool,
93
96
@@ -107,8 +110,8 @@ pub struct LanguageConfiguration {
107 #[serde(skip)]
110 #[serde(skip)]
108 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
111 pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
109 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
112 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
110− #[serde(skip_serializing_if = "Option::is_none")]
113+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
111− pub language_server: Option<LanguageServerConfiguration>,
114+ pub language_servers: Vec<LanguageServerFeatureConfiguration>,
112 #[serde(skip_serializing_if = "Option::is_none")]
115 #[serde(skip_serializing_if = "Option::is_none")]
113 pub indent: Option<IndentationConfiguration>,
116 pub indent: Option<IndentationConfiguration>,
114
117
@@ -208,6 +211,68 @@ impl<'de> Deserialize<'de> for FileType {
208 }
211 }
209}
212}
210
213
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+
211#[derive(Debug, Serialize, Deserialize)]
276#[derive(Debug, Serialize, Deserialize)]
212#[serde(rename_all = "kebab-case")]
277#[serde(rename_all = "kebab-case")]
213pub struct LanguageServerConfiguration {
278pub struct LanguageServerConfiguration {
@@ -217,9 +282,10 @@ pub struct LanguageServerConfiguration {
217 pub args: Vec<String>,
282 pub args: Vec<String>,
218 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
283 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
219 pub environment: HashMap<String, String>,
284 pub environment: HashMap<String, String>,
285+ #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
286+ pub config: Option<serde_json::Value>,
220 #[serde(default = "default_timeout")]
287 #[serde(default = "default_timeout")]
221 pub timeout: u64,
288 pub timeout: u64,
222− pub language_id: Option<String>,
223}
289}
224
290
225#[derive(Debug, Clone, Serialize, Deserialize)]
291#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -584,6 +650,15 @@ pub struct SoftWrap {
584 pub wrap_at_text_width: Option<bool>,
650 pub wrap_at_text_width: Option<bool>,
585}
651}
586
652
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+
587// Expose loader as Lazy<> global since it's always static?
662// Expose loader as Lazy<> global since it's always static?
588
663
589#[derive(Debug)]
664#[derive(Debug)]
@@ -594,6 +669,8 @@ pub struct Loader {
594 language_config_ids_by_suffix: HashMap<String, usize>,
669 language_config_ids_by_suffix: HashMap<String, usize>,
595 language_config_ids_by_shebang: HashMap<String, usize>,
670 language_config_ids_by_shebang: HashMap<String, usize>,
596
671
672+ language_server_configs: HashMap<String, LanguageServerConfiguration>,
673+
597 scopes: ArcSwap<Vec<String>>,
674 scopes: ArcSwap<Vec<String>>,
598}
675}
599
676
@@ -601,6 +678,7 @@ impl Loader {
601 pub fn new(config: Configuration) -> Self {
678 pub fn new(config: Configuration) -> Self {
602 let mut loader = Self {
679 let mut loader = Self {
603 language_configs: Vec::new(),
680 language_configs: Vec::new(),
681+ language_server_configs: config.language_server,
604 language_config_ids_by_extension: HashMap::new(),
682 language_config_ids_by_extension: HashMap::new(),
605 language_config_ids_by_suffix: HashMap::new(),
683 language_config_ids_by_suffix: HashMap::new(),
606 language_config_ids_by_shebang: HashMap::new(),
684 language_config_ids_by_shebang: HashMap::new(),
@@ -725,6 +803,10 @@ impl Loader {
725 self.language_configs.iter()
803 self.language_configs.iter()
726 }
804 }
727
805
806+ pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
807+ &self.language_server_configs
808+ }
809+
728 pub fn set_scopes(&self, scopes: Vec<String>) {
810 pub fn set_scopes(&self, scopes: Vec<String>) {
729 self.scopes.store(Arc::new(scopes));
811 self.scopes.store(Arc::new(scopes));
730
812
@@ -2370,7 +2452,10 @@ mod test {
2370 "#,
2452 "#,
2371 );
2453 );
2372
2454
2373− let loader = Loader::new(Configuration { language: vec![] });
2455+ let loader = Loader::new(Configuration {
2456+ language: vec![],
2457+ language_server: HashMap::new(),
2458+ });
2374 let language = get_language("rust").unwrap();
2459 let language = get_language("rust").unwrap();
2375
2460
2376 let query = Query::new(language, query_str).unwrap();
2461 let query = Query::new(language, query_str).unwrap();
@@ -2429,7 +2514,10 @@ mod test {
2429 .map(String::from)
2514 .map(String::from)
2430 .collect();
2515 .collect();
2431
2516
2432− let loader = Loader::new(Configuration { language: vec![] });
2517+ let loader = Loader::new(Configuration {
2518+ language: vec![],
2519+ language_server: HashMap::new(),
2520+ });
2433
2521
2434 let language = get_language("rust").unwrap();
2522 let language = get_language("rust").unwrap();
2435 let config = HighlightConfiguration::new(
2523 let config = HighlightConfiguration::new(
@@ -2532,7 +2620,10 @@ mod test {
2532 ) {
2620 ) {
2533 let source = Rope::from_str(source);
2621 let source = Rope::from_str(source);
2534
2622
2535− let loader = Loader::new(Configuration { language: vec![] });
2623+ let loader = Loader::new(Configuration {
2624+ language: vec![],
2625+ language_server: HashMap::new(),
2626+ });
2536 let language = get_language(language_name).unwrap();
2627 let language = get_language(language_name).unwrap();
2537
2628
2538 let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2629 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 {
44#[derive(Debug)]
44#[derive(Debug)]
45pub struct Client {
45pub struct Client {
46 id: usize,
46 id: usize,
47+ name: String,
47 _process: Child,
48 _process: Child,
48 server_tx: UnboundedSender<Payload>,
49 server_tx: UnboundedSender<Payload>,
49 request_counter: AtomicU64,
50 request_counter: AtomicU64,
@@ -166,8 +167,7 @@ impl Client {
166 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167 tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
167 }
168 }
168
169
169− #[allow(clippy::type_complexity)]
170+ #[allow(clippy::type_complexity, clippy::too_many_arguments)]
170− #[allow(clippy::too_many_arguments)]
171 pub fn start(
171 pub fn start(
172 cmd: &str,
172 cmd: &str,
173 args: &[String],
173 args: &[String],
@@ -176,6 +176,7 @@ impl Client {
176 root_markers: &[String],
176 root_markers: &[String],
177 manual_roots: &[PathBuf],
177 manual_roots: &[PathBuf],
178 id: usize,
178 id: usize,
179+ name: String,
179 req_timeout: u64,
180 req_timeout: u64,
180 doc_path: Option<&std::path::PathBuf>,
181 doc_path: Option<&std::path::PathBuf>,
181 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
182 ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@@ -200,7 +201,7 @@ impl Client {
200 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201 let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
201
202
202 let (server_rx, server_tx, initialize_notify) =
203 let (server_rx, server_tx, initialize_notify) =
203− Transport::start(reader, writer, stderr, id);
204+ Transport::start(reader, writer, stderr, id, name.clone());
204 let (workspace, workspace_is_cwd) = find_workspace();
205 let (workspace, workspace_is_cwd) = find_workspace();
205 let workspace = path::get_normalized_path(&workspace);
206 let workspace = path::get_normalized_path(&workspace);
206 let root = find_lsp_workspace(
207 let root = find_lsp_workspace(
@@ -225,6 +226,7 @@ impl Client {
225
226
226 let client = Self {
227 let client = Self {
227 id,
228 id,
229+ name,
228 _process: process,
230 _process: process,
229 server_tx,
231 server_tx,
230 request_counter: AtomicU64::new(0),
232 request_counter: AtomicU64::new(0),
@@ -240,6 +242,10 @@ impl Client {
240 Ok((client, server_rx, initialize_notify))
242 Ok((client, server_rx, initialize_notify))
241 }
243 }
242
244
245+ pub fn name(&self) -> &String {
246+ &self.name
247+ }
248+
243 pub fn id(&self) -> usize {
249 pub fn id(&self) -> usize {
244 self.id
250 self.id
245 }
251 }
helix-lsp/src/lib.rs+111−93
@@ -17,19 +17,16 @@ use helix_core::{
17use tokio::sync::mpsc::UnboundedReceiver;
17use tokio::sync::mpsc::UnboundedReceiver;
18
18
19use std::{
19use std::{
20− collections::{hash_map::Entry, HashMap},
20+ collections::HashMap,
21 path::{Path, PathBuf},
21 path::{Path, PathBuf},
22− sync::{
22+ sync::Arc,
23− atomic::{AtomicUsize, Ordering},
24− Arc,
25− },
26};
23};
27
24
28use thiserror::Error;
25use thiserror::Error;
29use tokio_stream::wrappers::UnboundedReceiverStream;
26use tokio_stream::wrappers::UnboundedReceiverStream;
30
27
31pub type Result<T> = core::result::Result<T, Error>;
28pub type Result<T> = core::result::Result<T, Error>;
32−type LanguageId = String;
29+type LanguageServerName = String;
33
30
34#[derive(Error, Debug)]
31#[derive(Error, Debug)]
35pub enum Error {
32pub enum Error {
@@ -49,7 +46,7 @@ pub enum Error {
49 Other(#[from] anyhow::Error),
46 Other(#[from] anyhow::Error),
50}
47}
51
48
52−#[derive(Clone, Copy, Debug, Default)]
49+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub enum OffsetEncoding {
50pub enum OffsetEncoding {
54 /// UTF-8 code units aka bytes
51 /// UTF-8 code units aka bytes
55 Utf8,
52 Utf8,
@@ -624,23 +621,18 @@ impl Notification {
624
621
625#[derive(Debug)]
622#[derive(Debug)]
626pub struct Registry {
623pub struct Registry {
627− inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
624+ inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
628−
625+ syn_loader: Arc<helix_core::syntax::Loader>,
629− counter: AtomicUsize,
626+ counter: usize,
630 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
627 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
631}
628}
632
629
633−impl Default for Registry {
634− fn default() -> Self {
635− Self::new()
636− }
637−}
638−
639impl Registry {
630impl Registry {
640− pub fn new() -> Self {
631+ pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
641 Self {
632 Self {
642 inner: HashMap::new(),
633 inner: HashMap::new(),
634+ syn_loader,
643− counter: AtomicUsize::new(0),
635+ counter: 0,
644 incoming: SelectAll::new(),
636 incoming: SelectAll::new(),
645 }
637 }
646 }
638 }
@@ -649,15 +641,43 @@ impl Registry {
649 self.inner
641 self.inner
650 .values()
642 .values()
651 .flatten()
643 .flatten()
652− .find(|(client_id, _)| client_id == &id)
644+ .find(|client| client.id() == id)
653− .map(|(_, client)| client.as_ref())
645+ .map(|client| &**client)
654 }
646 }
655
647
656 pub fn remove_by_id(&mut self, id: usize) {
648 pub fn remove_by_id(&mut self, id: usize) {
657− self.inner.retain(|_, clients| {
649+ self.inner.retain(|_, language_servers| {
658− clients.retain(|&(client_id, _)| client_id != id);
650+ language_servers.retain(|ls| id != ls.id());
659− !clients.is_empty()
651+ !language_servers.is_empty()
660− })
652+ });
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)
661 }
681 }
662
682
663 pub fn restart(
683 pub fn restart(
@@ -666,48 +686,46 @@ impl Registry {
666 doc_path: Option<&std::path::PathBuf>,
686 doc_path: Option<&std::path::PathBuf>,
667 root_dirs: &[PathBuf],
687 root_dirs: &[PathBuf],
668 enable_snippets: bool,
688 enable_snippets: bool,
669− ) -> Result<Option<Arc<Client>>> {
689+ ) -> Result<Vec<Arc<Client>>> {
670− let config = match &language_config.language_server {
690+ language_config
671− Some(config) => config,
691+ .language_servers
672− None => return Ok(None),
692+ .iter()
673− };
693+ .filter_map(|config| {
674−
694+ let name = config.name().clone();
675− let scope = language_config.scope.clone();
695+
676−
696+ #[allow(clippy::map_entry)]
677− match self.inner.entry(scope) {
697+ if self.inner.contains_key(&name) {
678− Entry::Vacant(_) => Ok(None),
698+ let client = match self.start_client(
679− Entry::Occupied(mut entry) => {
699+ name.clone(),
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,
700+ language_config,
686− config,
687− doc_path,
701+ doc_path,
688− root_dirs,
702+ root_dirs,
689− enable_snippets,
703+ enable_snippets,
704+ ) {
705+ Ok(client) => client,
706+ error => return Some(error),
690− )?;
707+ };
691− self.incoming.push(UnboundedReceiverStream::new(incoming));
708+ let old_clients = self.inner.insert(name, vec![client.clone()]).unwrap();
692
709
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+ }
694
717
695− for (_, old_client) in old_clients {
718+ Some(Ok(client))
696− tokio::spawn(async move {
719+ } else {
697− let _ = old_client.force_shutdown().await;
720+ None
698− });
699 }
721 }
700−
722+ })
701− Ok(Some(client))
723+ .collect()
702− }
703− }
704 }
724 }
705
725
706− pub fn stop(&mut self, language_config: &LanguageConfiguration) {
726+ pub fn stop(&mut self, name: &str) {
707− let scope = language_config.scope.clone();
708−
709− if let Some(clients) = self.inner.remove(&scope) {
727+ if let Some(clients) = self.inner.remove(name) {
710− for (_, client) in clients {
728+ for client in clients {
711 tokio::spawn(async move {
729 tokio::spawn(async move {
712 let _ = client.force_shutdown().await;
730 let _ = client.force_shutdown().await;
713 });
731 });
@@ -721,37 +739,35 @@ impl Registry {
721 doc_path: Option<&std::path::PathBuf>,
739 doc_path: Option<&std::path::PathBuf>,
722 root_dirs: &[PathBuf],
740 root_dirs: &[PathBuf],
723 enable_snippets: bool,
741 enable_snippets: bool,
724− ) -> Result<Option<Arc<Client>>> {
742+ ) -> Result<Vec<Arc<Client>>> {
725− let config = match &language_config.language_server {
743+ language_config
726− Some(config) => config,
744+ .language_servers
727− None => return Ok(None),
745+ .iter()
728− };
746+ .map(|features| {
729−
747+ let name = features.name();
730− let clients = self.inner.entry(language_config.scope.clone()).or_default();
748+ if let Some(clients) = self.inner.get_mut(name) {
731− // check if we already have a client for this documents root that we can reuse
732− if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
749+ if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, client)| {
733− client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
750+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
734− }) {
751+ }) {
735− return Ok(Some(client.1.clone()));
752+ return Ok(client.clone());
736− }
753+ }
737− // initialize a new client
754+ }
738− let id = self.counter.fetch_add(1, Ordering::Relaxed);
755+ let client = self.start_client(
739−
756+ name.clone(),
740− let NewClientResult(client, incoming) = start_client(
741− id,
742− language_config,
757+ language_config,
743− config,
744− doc_path,
758+ doc_path,
745− root_dirs,
759+ root_dirs,
746− enable_snippets,
760+ enable_snippets,
747− )?;
761+ )?;
748− clients.push((id, client.clone()));
762+ let clients = self.inner.entry(features.name().clone()).or_default();
749− self.incoming.push(UnboundedReceiverStream::new(incoming));
763+ clients.push(client.clone());
750− Ok(Some(client))
764+ Ok(client)
765+ })
766+ .collect()
751 }
767 }
752
768
753 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
769 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
754− self.inner.values().flatten().map(|(_, client)| client)
770+ self.inner.values().flatten()
755 }
771 }
756}
772}
757
773
@@ -833,26 +849,28 @@ impl LspProgressMap {
833 }
849 }
834}
850}
835
851
836−struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
852+struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
837
853
838/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
854/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
839/// it is only called when it makes sense.
855/// it is only called when it makes sense.
840fn start_client(
856fn start_client(
841 id: usize,
857 id: usize,
858+ name: String,
842 config: &LanguageConfiguration,
859 config: &LanguageConfiguration,
843 ls_config: &LanguageServerConfiguration,
860 ls_config: &LanguageServerConfiguration,
844 doc_path: Option<&std::path::PathBuf>,
861 doc_path: Option<&std::path::PathBuf>,
845 root_dirs: &[PathBuf],
862 root_dirs: &[PathBuf],
846 enable_snippets: bool,
863 enable_snippets: bool,
847−) -> Result<NewClientResult> {
864+) -> Result<NewClient> {
848 let (client, incoming, initialize_notify) = Client::start(
865 let (client, incoming, initialize_notify) = Client::start(
849 &ls_config.command,
866 &ls_config.command,
850 &ls_config.args,
867 &ls_config.args,
851− config.config.clone(),
868+ ls_config.config.clone(),
852 ls_config.environment.clone(),
869 ls_config.environment.clone(),
853 &config.roots,
870 &config.roots,
854 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
871 config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
855 id,
872 id,
873+ name,
856 ls_config.timeout,
874 ls_config.timeout,
857 doc_path,
875 doc_path,
858 )?;
876 )?;
@@ -886,7 +904,7 @@ fn start_client(
886 initialize_notify.notify_one();
904 initialize_notify.notify_one();
887 });
905 });
888
906
889− Ok(NewClientResult(client, incoming))
907+ Ok(NewClient(client, incoming))
890}
908}
891
909
892/// Find an LSP workspace of a file using the following mechanism:
910/// Find an LSP workspace of a file using the following mechanism:
helix-lsp/src/transport.rs+43−20
@@ -38,6 +38,7 @@ enum ServerMessage {
38#[derive(Debug)]
38#[derive(Debug)]
39pub struct Transport {
39pub struct Transport {
40 id: usize,
40 id: usize,
41+ name: String,
41 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
42 pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
42}
43}
43
44
@@ -47,6 +48,7 @@ impl Transport {
47 server_stdin: BufWriter<ChildStdin>,
48 server_stdin: BufWriter<ChildStdin>,
48 server_stderr: BufReader<ChildStderr>,
49 server_stderr: BufReader<ChildStderr>,
49 id: usize,
50 id: usize,
51+ name: String,
50 ) -> (
52 ) -> (
51 UnboundedReceiver<(usize, jsonrpc::Call)>,
53 UnboundedReceiver<(usize, jsonrpc::Call)>,
52 UnboundedSender<Payload>,
54 UnboundedSender<Payload>,
@@ -58,6 +60,7 @@ impl Transport {
58
60
59 let transport = Self {
61 let transport = Self {
60 id,
62 id,
63+ name,
61 pending_requests: Mutex::new(HashMap::default()),
64 pending_requests: Mutex::new(HashMap::default()),
62 };
65 };
63
66
@@ -83,6 +86,7 @@ impl Transport {
83 async fn recv_server_message(
86 async fn recv_server_message(
84 reader: &mut (impl AsyncBufRead + Unpin + Send),
87 reader: &mut (impl AsyncBufRead + Unpin + Send),
85 buffer: &mut String,
88 buffer: &mut String,
89+ language_server_name: &str,
86 ) -> Result<ServerMessage> {
90 ) -> Result<ServerMessage> {
87 let mut content_length = None;
91 let mut content_length = None;
88 loop {
92 loop {
@@ -124,7 +128,7 @@ impl Transport {
124 reader.read_exact(&mut content).await?;
128 reader.read_exact(&mut content).await?;
125 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
129 let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
126
130
127− info!("<- {}", msg);
131+ info!("{language_server_name} <- {msg}");
128
132
129 // try parsing as output (server response) or call (server request)
133 // try parsing as output (server response) or call (server request)
130 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
134 let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@@ -135,12 +139,13 @@ impl Transport {
135 async fn recv_server_error(
139 async fn recv_server_error(
136 err: &mut (impl AsyncBufRead + Unpin + Send),
140 err: &mut (impl AsyncBufRead + Unpin + Send),
137 buffer: &mut String,
141 buffer: &mut String,
142+ language_server_name: &str,
138 ) -> Result<()> {
143 ) -> Result<()> {
139 buffer.truncate(0);
144 buffer.truncate(0);
140 if err.read_line(buffer).await? == 0 {
145 if err.read_line(buffer).await? == 0 {
141 return Err(Error::StreamClosed);
146 return Err(Error::StreamClosed);
142 };
147 };
143− error!("err <- {:?}", buffer);
148+ error!("{language_server_name} err <- {buffer:?}");
144
149
145 Ok(())
150 Ok(())
146 }
151 }
@@ -162,15 +167,17 @@ impl Transport {
162 Payload::Notification(value) => serde_json::to_string(&value)?,
167 Payload::Notification(value) => serde_json::to_string(&value)?,
163 Payload::Response(error) => serde_json::to_string(&error)?,
168 Payload::Response(error) => serde_json::to_string(&error)?,
164 };
169 };
165− self.send_string_to_server(server_stdin, json).await
170+ self.send_string_to_server(server_stdin, json, &self.name)
171+ .await
166 }
172 }
167
173
168 async fn send_string_to_server(
174 async fn send_string_to_server(
169 &self,
175 &self,
170 server_stdin: &mut BufWriter<ChildStdin>,
176 server_stdin: &mut BufWriter<ChildStdin>,
171 request: String,
177 request: String,
178+ language_server_name: &str,
172 ) -> Result<()> {
179 ) -> Result<()> {
173− info!("-> {}", request);
180+ info!("{language_server_name} -> {request}");
174
181
175 // send the headers
182 // send the headers
176 server_stdin
183 server_stdin
@@ -189,9 +196,13 @@ impl Transport {
189 &self,
196 &self,
190 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
197 client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
191 msg: ServerMessage,
198 msg: ServerMessage,
199+ language_server_name: &str,
192 ) -> Result<()> {
200 ) -> Result<()> {
193 match msg {
201 match msg {
202+ ServerMessage::Output(output) => {
194− ServerMessage::Output(output) => self.process_request_response(output).await?,
203+ self.process_request_response(output, language_server_name)
204+ .await?
205+ }
195 ServerMessage::Call(call) => {
206 ServerMessage::Call(call) => {
196 client_tx
207 client_tx
197 .send((self.id, call))
208 .send((self.id, call))
@@ -202,14 +213,18 @@ impl Transport {
202 Ok(())
213 Ok(())
203 }
214 }
204
215
205− async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
216+ async fn process_request_response(
217+ &self,
218+ output: jsonrpc::Output,
219+ language_server_name: &str,
220+ ) -> Result<()> {
206 let (id, result) = match output {
221 let (id, result) = match output {
207 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
222 jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
208− info!("<- {}", result);
223+ info!("{language_server_name} <- {}", result);
209 (id, Ok(result))
224 (id, Ok(result))
210 }
225 }
211 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
226 jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
212− error!("<- {}", error);
227+ error!("{language_server_name} <- {error}");
213 (id, Err(error.into()))
228 (id, Err(error.into()))
214 }
229 }
215 };
230 };
@@ -240,12 +255,17 @@ impl Transport {
240 ) {
255 ) {
241 let mut recv_buffer = String::new();
256 let mut recv_buffer = String::new();
242 loop {
257 loop {
243− match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
258+ match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
259+ .await
260+ {
244 Ok(msg) => {
261 Ok(msg) => {
262+ match transport
245− match transport.process_server_message(&client_tx, msg).await {
263+ .process_server_message(&client_tx, msg, &transport.name)
264+ .await
265+ {
246 Ok(_) => {}
266 Ok(_) => {}
247 Err(err) => {
267 Err(err) => {
248− error!("err: <- {:?}", err);
268+ error!("{} err: <- {err:?}", transport.name);
249 break;
269 break;
250 }
270 }
251 };
271 };
@@ -270,7 +290,7 @@ impl Transport {
270 params: jsonrpc::Params::None,
290 params: jsonrpc::Params::None,
271 }));
291 }));
272 match transport
292 match transport
273− .process_server_message(&client_tx, notification)
293+ .process_server_message(&client_tx, notification, &transport.name)
274 .await
294 .await
275 {
295 {
276 Ok(_) => {}
296 Ok(_) => {}
@@ -281,20 +301,22 @@ impl Transport {
281 break;
301 break;
282 }
302 }
283 Err(err) => {
303 Err(err) => {
284− error!("err: <- {:?}", err);
304+ error!("{} err: <- {err:?}", transport.name);
285 break;
305 break;
286 }
306 }
287 }
307 }
288 }
308 }
289 }
309 }
290
310
291− async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
311+ async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
292 let mut recv_buffer = String::new();
312 let mut recv_buffer = String::new();
293 loop {
313 loop {
294− match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
314+ match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
315+ .await
316+ {
295 Ok(_) => {}
317 Ok(_) => {}
296 Err(err) => {
318 Err(err) => {
297− error!("err: <- {:?}", err);
319+ error!("{} err: <- {err:?}", transport.name);
298 break;
320 break;
299 }
321 }
300 }
322 }
@@ -348,10 +370,11 @@ impl Transport {
348 method: lsp_types::notification::Initialized::METHOD.to_string(),
370 method: lsp_types::notification::Initialized::METHOD.to_string(),
349 params: jsonrpc::Params::None,
371 params: jsonrpc::Params::None,
350 }));
372 }));
373+ let language_server_name = &transport.name;
351− match transport.process_server_message(&client_tx, notification).await {
374+ match transport.process_server_message(&client_tx, notification, language_server_name).await {
352 Ok(_) => {}
375 Ok(_) => {}
353 Err(err) => {
376 Err(err) => {
354− error!("err: <- {:?}", err);
377+ error!("{language_server_name} err: <- {err:?}");
355 }
378 }
356 }
379 }
357
380
@@ -361,7 +384,7 @@ impl Transport {
361 match transport.send_payload_to_server(&mut server_stdin, msg).await {
384 match transport.send_payload_to_server(&mut server_stdin, msg).await {
362 Ok(_) => {}
385 Ok(_) => {}
363 Err(err) => {
386 Err(err) => {
364− error!("err: <- {:?}", err);
387+ error!("{language_server_name} err: <- {err:?}");
365 }
388 }
366 }
389 }
367 }
390 }
@@ -380,7 +403,7 @@ impl Transport {
380 match transport.send_payload_to_server(&mut server_stdin, msg).await {
403 match transport.send_payload_to_server(&mut server_stdin, msg).await {
381 Ok(_) => {}
404 Ok(_) => {}
382 Err(err) => {
405 Err(err) => {
383− error!("err: <- {:?}", err);
406+ error!("{} err: <- {err:?}", transport.name);
384 }
407 }
385 }
408 }
386 }
409 }
helix-term/src/application.rs+62−64
@@ -30,6 +30,7 @@ use crate::{
30
30
31use log::{debug, error, warn};
31use log::{debug, error, warn};
32use std::{
32use std::{
33+ collections::btree_map::Entry,
33 io::{stdin, stdout},
34 io::{stdin, stdout},
34 path::Path,
35 path::Path,
35 sync::Arc,
36 sync::Arc,
@@ -564,7 +565,7 @@ impl Application {
564 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565 let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
565 let id = doc.id();
566 let id = doc.id();
566 doc.detect_language(loader);
567 doc.detect_language(loader);
567− let _ = self.editor.refresh_language_server(id);
568+ self.editor.refresh_language_servers(id);
568 }
569 }
569
570
570 // TODO: fix being overwritten by lsp
571 // TODO: fix being overwritten by lsp
@@ -662,6 +663,18 @@ impl Application {
662 ) {
663 ) {
663 use helix_lsp::{Call, MethodCall, Notification};
664 use helix_lsp::{Call, MethodCall, Notification};
664
665
666+ macro_rules! language_server {
667+ () => {
668+ match self.editor.language_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+
665 match call {
678 match call {
666 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
679 Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
667 let notification = match Notification::parse(&method, params) {
680 let notification = match Notification::parse(&method, params) {
@@ -677,14 +690,7 @@ impl Application {
677
690
678 match notification {
691 match notification {
679 Notification::Initialized => {
692 Notification::Initialized => {
680− let language_server =
693+ let language_server = language_server!();
681− match self.editor.language_servers.get_by_id(server_id) {
682− Some(language_server) => language_server,
683− None => {
684− warn!("can't find language server with id `{}`", server_id);
685− return;
686− }
687− };
688
694
689 // Trigger a workspace/didChangeConfiguration notification after initialization.
695 // Trigger a workspace/didChangeConfiguration notification after initialization.
690 // This might not be required by the spec but Neovim does this as well, so it's
696 // This might not be required by the spec but Neovim does this as well, so it's
@@ -694,7 +700,7 @@ impl Application {
694 }
700 }
695
701
696 let docs = self.editor.documents().filter(|doc| {
702 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)
698 });
704 });
699
705
700 // trigger textDocument/didOpen for docs that are already open
706 // trigger textDocument/didOpen for docs that are already open
@@ -723,6 +729,7 @@ impl Application {
723 return;
729 return;
724 }
730 }
725 };
731 };
732+ let offset_encoding = language_server!().offset_encoding();
726 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
733 let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
727 if let Some(version) = params.version {
734 if let Some(version) = params.version {
728 if version != doc.version() {
735 if version != doc.version() {
@@ -745,18 +752,11 @@ impl Application {
745 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
752 use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
746 use lsp::DiagnosticSeverity;
753 use lsp::DiagnosticSeverity;
747
754
748− let language_server = if let Some(language_server) = doc.language_server() {
749− language_server
750− } else {
751− log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
752− return None;
753− };
754−
755 // TODO: convert inside server
755 // TODO: convert inside server
756 let start = if let Some(start) = lsp_pos_to_pos(
756 let start = if let Some(start) = lsp_pos_to_pos(
757 text,
757 text,
758 diagnostic.range.start,
758 diagnostic.range.start,
759− language_server.offset_encoding(),
759+ offset_encoding,
760 ) {
760 ) {
761 start
761 start
762 } else {
762 } else {
@@ -764,11 +764,9 @@ impl Application {
764 return None;
764 return None;
765 };
765 };
766
766
767− let end = if let Some(end) = lsp_pos_to_pos(
767+ let end = if let Some(end) =
768− text,
769− diagnostic.range.end,
768+ lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
770− language_server.offset_encoding(),
771− ) {
769+ {
772 end
770 end
773 } else {
771 } else {
774 log::warn!("lsp position out of bounds - {:?}", diagnostic);
772 log::warn!("lsp position out of bounds - {:?}", diagnostic);
@@ -807,14 +805,19 @@ impl Application {
807 None => None,
805 None => None,
808 };
806 };
809
807
810− let tags = if let Some(ref tags) = diagnostic.tags {
808+ let tags = if let Some(tags) = &diagnostic.tags {
811− let new_tags = tags.iter().filter_map(|tag| {
809+ let new_tags = tags
810+ .iter()
812− match *tag {
811+ .filter_map(|tag| match *tag {
812+ lsp::DiagnosticTag::DEPRECATED => {
813− lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
813+ Some(DiagnosticTag::Deprecated)
814+ }
815+ lsp::DiagnosticTag::UNNECESSARY => {
814− lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
816+ Some(DiagnosticTag::Unnecessary)
817+ }
815− _ => None
818+ _ => None,
816− }
819+ })
817− }).collect();
820+ .collect();
818
821
819 new_tags
822 new_tags
820 } else {
823 } else {
@@ -830,11 +833,12 @@ impl Application {
830 tags,
833 tags,
831 source: diagnostic.source.clone(),
834 source: diagnostic.source.clone(),
832 data: diagnostic.data.clone(),
835 data: diagnostic.data.clone(),
836+ language_server_id: server_id,
833 })
837 })
834 })
838 })
835 .collect();
839 .collect();
836
840
837− doc.set_diagnostics(diagnostics);
841+ doc.replace_diagnostics(diagnostics, server_id);
838 }
842 }
839
843
840 // Sort diagnostics first by severity and then by line numbers.
844 // Sort diagnostics first by severity and then by line numbers.
@@ -842,13 +846,26 @@ impl Application {
842 params
846 params
843 .diagnostics
847 .diagnostics
844 .sort_unstable_by_key(|d| (d.severity, d.range.start));
848 .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();
845
854
846 // Insert the original lsp::Diagnostics here because we may have no open document
855 // Insert the original lsp::Diagnostics here because we may have no open document
847 // for diagnosic message and so we can't calculate the exact position.
856 // for diagnosic message and so we can't calculate the exact position.
848 // When using them later in the diagnostics picker, we calculate them on-demand.
857 // When using them later in the diagnostics picker, we calculate them on-demand.
849− self.editor
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);
850− .diagnostics
863+ current_diagnostics.extend(diagnostics);
864+ }
865+ Entry::Vacant(v) => {
851− .insert(params.uri, params.diagnostics);
866+ v.insert(diagnostics);
867+ }
868+ };
852 }
869 }
853 Notification::ShowMessage(params) => {
870 Notification::ShowMessage(params) => {
854 log::warn!("unhandled window/showMessage: {:?}", params);
871 log::warn!("unhandled window/showMessage: {:?}", params);
@@ -950,10 +967,12 @@ impl Application {
950 .editor
967 .editor
951 .documents_mut()
968 .documents_mut()
952 .filter_map(|doc| {
969 .filter_map(|doc| {
953− if doc.language_server().map(|server| server.id())
970+ if doc
971+ .language_servers()
972+ .iter()
954− == Some(server_id)
973+ .any(|server| server.id() == server_id)
955 {
974 {
956− doc.set_diagnostics(Vec::new());
975+ doc.clear_diagnostics(server_id);
957 doc.url()
976 doc.url()
958 } else {
977 } else {
959 None
978 None
@@ -1029,28 +1048,15 @@ impl Application {
1029 }))
1048 }))
1030 }
1049 }
1031 Ok(MethodCall::WorkspaceFolders) => {
1050 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))
1036 }
1052 }
1037 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1053 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1054+ let language_server = language_server!();
1038 let result: Vec<_> = params
1055 let result: Vec<_> = params
1039 .items
1056 .items
1040 .iter()
1057 .iter()
1041− .map(|item| {
1058+ .filter_map(|item| {
1042− let mut config = match &item.scope_uri {
1059+ let mut config = language_server.config()?;
1043− Some(scope) => {
1044− let path = scope.to_file_path().ok()?;
1045− let doc = self.editor.document_by_path(path)?;
1046− doc.language_config()?.config.as_ref()?
1047− }
1048− None => self
1049− .editor
1050− .language_servers
1051− .get_by_id(server_id)?
1052− .config()?,
1053− };
1054 if let Some(section) = item.section.as_ref() {
1060 if let Some(section) = item.section.as_ref() {
1055 for part in section.split('.') {
1061 for part in section.split('.') {
1056 config = config.get(part)?;
1062 config = config.get(part)?;
@@ -1074,15 +1080,7 @@ impl Application {
1074 }
1080 }
1075 };
1081 };
1076
1082
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));
1086 }
1084 }
1087 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
1085 Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
1088 }
1086 }
helix-term/src/commands.rs+154−119
@@ -23,6 +23,7 @@ use helix_core::{
23 regex::{self, Regex, RegexBuilder},
23 regex::{self, Regex, RegexBuilder},
24 search::{self, CharMatcher},
24 search::{self, CharMatcher},
25 selection, shellwords, surround,
25 selection, shellwords, surround,
26+ syntax::LanguageServerFeature,
26 text_annotations::TextAnnotations,
27 text_annotations::TextAnnotations,
27 textobject,
28 textobject,
28 tree_sitter::Node,
29 tree_sitter::Node,
@@ -54,13 +55,13 @@ use crate::{
54 job::Callback,
55 job::Callback,
55 keymap::ReverseKeymap,
56 keymap::ReverseKeymap,
56 ui::{
57 ui::{
57− self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
58+ self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
58− Popup, Prompt, PromptEvent,
59+ FilePicker, Picker, Popup, Prompt, PromptEvent,
59 },
60 },
60};
61};
61
62
62use crate::job::{self, Jobs};
63use crate::job::{self, Jobs};
63−use futures_util::StreamExt;
64+use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
64use std::{collections::HashMap, fmt, future::Future};
65use std::{collections::HashMap, fmt, future::Future};
65use std::{collections::HashSet, num::NonZeroUsize};
66use std::{collections::HashSet, num::NonZeroUsize};
66
67
@@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
3029
3030
3030fn goto_first_diag(cx: &mut Context) {
3031fn goto_first_diag(cx: &mut Context) {
3031 let (view, doc) = current!(cx.editor);
3032 let (view, doc) = current!(cx.editor);
3032− let selection = match doc.diagnostics().first() {
3033+ let selection = match doc.shown_diagnostics().next() {
3033 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3034 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3034 None => return,
3035 None => return,
3035 };
3036 };
@@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
3038
3039
3039fn goto_last_diag(cx: &mut Context) {
3040fn goto_last_diag(cx: &mut Context) {
3040 let (view, doc) = current!(cx.editor);
3041 let (view, doc) = current!(cx.editor);
3041− let selection = match doc.diagnostics().last() {
3042+ let selection = match doc.shown_diagnostics().last() {
3042 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3043 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3043 None => return,
3044 None => return,
3044 };
3045 };
@@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
3054 .cursor(doc.text().slice(..));
3055 .cursor(doc.text().slice(..));
3055
3056
3056 let diag = doc
3057 let diag = doc
3057− .diagnostics()
3058+ .shown_diagnostics()
3058− .iter()
3059 .find(|diag| diag.range.start > cursor_pos)
3059 .find(|diag| diag.range.start > cursor_pos)
3060− .or_else(|| doc.diagnostics().first());
3060+ .or_else(|| doc.shown_diagnostics().next());
3061
3061
3062 let selection = match diag {
3062 let selection = match diag {
3063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
3063 Some(diag) => Selection::single(diag.range.start, diag.range.end),
@@ -3075,11 +3075,12 @@ fn goto_prev_diag(cx: &mut Context) {
3075 .cursor(doc.text().slice(..));
3075 .cursor(doc.text().slice(..));
3076
3076
3077 let diag = doc
3077 let diag = doc
3078− .diagnostics()
3078+ .shown_diagnostics()
3079− .iter()
3079+ .collect::<Vec<_>>()
3080+ .into_iter()
3080 .rev()
3081 .rev()
3081 .find(|diag| diag.range.start < cursor_pos)
3082 .find(|diag| diag.range.start < cursor_pos)
3082− .or_else(|| doc.diagnostics().last());
3083+ .or_else(|| doc.shown_diagnostics().last());
3083
3084
3084 let selection = match diag {
3085 let selection = match diag {
3085 // NOTE: the selection is reversed because we're jumping to the
3086 // NOTE: the selection is reversed because we're jumping to the
@@ -3234,60 +3235,72 @@ pub mod insert {
3234 use helix_lsp::lsp;
3235 use helix_lsp::lsp;
3235 // if ch matches completion char, trigger completion
3236 // if ch matches completion char, trigger completion
3236 let doc = doc_mut!(cx.editor);
3237 let doc = doc_mut!(cx.editor);
3237− let language_server = match doc.language_server() {
3238+ let trigger_completion = doc
3238− Some(language_server) => language_server,
3239+ .language_servers_with_feature(LanguageServerFeature::Completion)
3239− None => return,
3240+ .iter()
3240− };
3241+ .any(|ls| {
3242+ let capabilities = ls.capabilities();
3241
3243
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+ });
3243
3250
3244− if let Some(lsp::CompletionOptions {
3251+ if trigger_completion {
3245− trigger_characters: Some(triggers),
3246− ..
3247− }) = &capabilities.completion_provider
3248− {
3249− // TODO: what if trigger is multiple chars long
3250− if triggers.iter().any(|trigger| trigger.contains(ch)) {
3251− cx.editor.clear_idle_timer();
3252+ cx.editor.clear_idle_timer();
3252− super::completion(cx);
3253+ super::completion(cx);
3253− }
3254 }
3254 }
3255 }
3255 }
3256
3256
3257 fn signature_help(cx: &mut Context, ch: char) {
3257 fn signature_help(cx: &mut Context, ch: char) {
3258+ use futures_util::FutureExt;
3258 use helix_lsp::lsp;
3259 use helix_lsp::lsp;
3259 // if ch matches signature_help char, trigger
3260 // if ch matches signature_help char, trigger
3260− let doc = doc_mut!(cx.editor);
3261+ let (view, doc) = current!(cx.editor);
3261− // The language_server!() macro is not used here since it will
3262+ // lsp doesn't tell us when to close the signature help, so we request
3262− // print an "LSP not active for current buffer" message on
3263+ // the help information again after common close triggers which should
3263− // every keypress.
3264+ // return None, which in turn closes the popup.
3264− let language_server = match doc.language_server() {
3265+ let close_triggers = &[')', ';', '.'];
3265− Some(language_server) => language_server,
3266+ // TODO support multiple language servers (not just the first that is found)
3266− None => return,
3267+ let future = doc
3267− };
3268+ .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
3268−
3269+ .iter()
3269− let capabilities = language_server.capabilities();
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+ });
3270
3297
3271− if let lsp::ServerCapabilities {
3298+ if let Some(future) = future {
3272− signature_help_provider:
3299+ super::signature_help_impl_with_future(
3273− Some(lsp::SignatureHelpOptions {
3300+ cx,
3274− trigger_characters: Some(triggers),
3301+ future.boxed(),
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);
3302+ SignatureHelpInvoked::Automatic,
3290− }
3303+ )
3291 }
3304 }
3292 }
3305 }
3293
3306
@@ -3301,7 +3314,7 @@ pub mod insert {
3301 Some(transaction)
3314 Some(transaction)
3302 }
3315 }
3303
3316
3304− use helix_core::auto_pairs;
3317+ use helix_core::{auto_pairs, syntax::LanguageServerFeature};
3305
3318
3306 pub fn insert_char(cx: &mut Context, c: char) {
3319 pub fn insert_char(cx: &mut Context, c: char) {
3307 let (view, doc) = current_ref!(cx.editor);
3320 let (view, doc) = current_ref!(cx.editor);
@@ -4046,55 +4059,55 @@ fn format_selections(cx: &mut Context) {
4046 use helix_lsp::{lsp, util::range_to_lsp_range};
4059 use helix_lsp::{lsp, util::range_to_lsp_range};
4047
4060
4048 let (view, doc) = current!(cx.editor);
4061 let (view, doc) = current!(cx.editor);
4062+ let view_id = view.id;
4049
4063
4050 // via lsp if available
4064 // via lsp if available
4051 // TODO: else via tree-sitter indentation calculations
4065 // TODO: else via tree-sitter indentation calculations
4052
4066
4053− let language_server = match doc.language_server() {
4067+ if doc.selection(view_id).len() != 1 {
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 {
4065 cx.editor
4068 cx.editor
4066 .set_error("format_selections only supports a single selection for now");
4069 .set_error("format_selections only supports a single selection for now");
4067 return;
4070 return;
4068 }
4071 }
4069
4072
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+
4070− // TODO: handle fails
4084+ // TODO: handle fails
4071− // TODO: concurrent map over all ranges
4085+ // TODO: concurrent map over all ranges
4072−
4086+
4073− let range = ranges[0];
4087+ let range = ranges[0];
4074−
4088+
4075− let request = match language_server.text_document_range_formatting(
4089+ let future = language_server.text_document_range_formatting(
4076− doc.identifier(),
4090+ doc.identifier(),
4077− range,
4091+ range,
4078− lsp::FormattingOptions::default(),
4092+ lsp::FormattingOptions::default(),
4079− None,
4093+ None,
4080− ) {
4094+ )?;
4081− Some(future) => future,
4095+ Some((future, offset_encoding))
4096+ }) {
4097+ Some(future_offset_encoding) => future_offset_encoding,
4082 None => {
4098 None => {
4083 cx.editor
4099 cx.editor
4084− .set_error("Language server does not support range formatting");
4100+ .set_error("No language server supports range formatting");
4085 return;
4101 return;
4086 }
4102 }
4087 };
4103 };
4088
4104
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();
4090
4106
4107+ let transaction =
4091− let transaction = helix_lsp::util::generate_transaction_from_edits(
4108+ helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
4092− doc.text(),
4093− edits,
4094− language_server.offset_encoding(),
4095− );
4096
4109
4097− doc.apply(&transaction, view.id);
4110+ doc.apply(&transaction, view_id);
4098}
4111}
4099
4112
4100fn join_selections_impl(cx: &mut Context, select_space: bool) {
4113fn join_selections_impl(cx: &mut Context, select_space: bool) {
@@ -4231,21 +4244,45 @@ pub fn completion(cx: &mut Context) {
4231 doc.savepoint(view)
4244 doc.savepoint(view)
4232 };
4245 };
4233
4246
4234− let language_server = match doc.language_server() {
4235− Some(language_server) => language_server,
4236− None => return,
4237− };
4238−
4239− let offset_encoding = language_server.offset_encoding();
4240 let text = savepoint.text.clone();
4247 let text = savepoint.text.clone();
4241 let cursor = savepoint.cursor();
4248 let cursor = savepoint.cursor();
4242
4249
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();
4243− let pos = pos_to_lsp_pos(&text, cursor, 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();
4244
4281
4245− let future = match language_server.completion(doc.identifier(), pos, None) {
4282+ anyhow::Ok(items)
4246− Some(future) => future,
4283+ })
4247− None => return,
4284+ })
4248− };
4285+ .collect();
4249
4286
4250 // setup a channel that allows the request to be canceled
4287 // setup a channel that allows the request to be canceled
4251 let (tx, rx) = oneshot::channel();
4288 let (tx, rx) = oneshot::channel();
@@ -4254,12 +4291,20 @@ pub fn completion(cx: &mut Context) {
4254 // and the associated request is automatically dropped
4291 // and the associated request is automatically dropped
4255 cx.editor.completion_request_handle = Some(tx);
4292 cx.editor.completion_request_handle = Some(tx);
4256 let future = async move {
4293 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+ };
4257 tokio::select! {
4302 tokio::select! {
4258 biased;
4303 biased;
4259 _ = rx => {
4304 _ = rx => {
4260− Ok(serde_json::Value::Null)
4305+ Ok(Vec::new())
4261 }
4306 }
4262− res = future => {
4307+ res = items_future => {
4263 res
4308 res
4264 }
4309 }
4265 }
4310 }
@@ -4293,9 +4338,9 @@ pub fn completion(cx: &mut Context) {
4293 },
4338 },
4294 ));
4339 ));
4295
4340
4296− cx.callback(
4341+ cx.jobs.callback(async move {
4297− future,
4342+ let items = future.await?;
4298− move |editor, compositor, response: Option<lsp::CompletionResponse>| {
4343+ let call = move |editor: &mut Editor, compositor: &mut Compositor| {
4299 let (view, doc) = current_ref!(editor);
4344 let (view, doc) = current_ref!(editor);
4300 // check if the completion request is stale.
4345 // check if the completion request is stale.
4301 //
4346 //
@@ -4306,16 +4351,6 @@ pub fn completion(cx: &mut Context) {
4306 return;
4351 return;
4307 }
4352 }
4308
4353
4309− let items = match response {
4310− Some(lsp::CompletionResponse::Array(items)) => items,
4311− // TODO: do something with is_incomplete
4312− Some(lsp::CompletionResponse::List(lsp::CompletionList {
4313− is_incomplete: _is_incomplete,
4314− items,
4315− })) => items,
4316− None => Vec::new(),
4317− };
4318−
4319 if items.is_empty() {
4354 if items.is_empty() {
4320 // editor.set_error("No completion available");
4355 // editor.set_error("No completion available");
4321 return;
4356 return;
@@ -4326,7 +4361,6 @@ pub fn completion(cx: &mut Context) {
4326 editor,
4361 editor,
4327 savepoint,
4362 savepoint,
4328 items,
4363 items,
4329− offset_encoding,
4330 start_offset,
4364 start_offset,
4331 trigger_offset,
4365 trigger_offset,
4332 size,
4366 size,
@@ -4340,8 +4374,9 @@ pub fn completion(cx: &mut Context) {
4340 {
4374 {
4341 compositor.remove(SignatureHelp::ID);
4375 compositor.remove(SignatureHelp::ID);
4342 }
4376 }
4343− },
4377+ };
4378+ Ok(Callback::EditorCompositor(Box::new(call)))
4344− );
4379+ });
4345}
4380}
4346
4381
4347// comments
4382// comments
@@ -5141,7 +5176,7 @@ async fn shell_impl_async(
5141 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
5176 helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
5142 .await?;
5177 .await?;
5143 }
5178 }
5144− Ok::<_, anyhow::Error>(())
5179+ anyhow::Ok(())
5145 });
5180 });
5146 let (output, _) = tokio::join! {
5181 let (output, _) = tokio::join! {
5147 process.wait_with_output(),
5182 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};
2use helix_lsp::{
2use helix_lsp::{
3 block_on,
3 block_on,
4 lsp::{
4 lsp::{
@@ -8,6 +8,8 @@ use helix_lsp::{
8 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
8 util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
9 OffsetEncoding,
9 OffsetEncoding,
10};
10};
11+use serde_json::Value;
12+use tokio_stream::StreamExt;
11use tui::{
13use tui::{
12 text::{Span, Spans},
14 text::{Span, Spans},
13 widgets::Row,
15 widgets::Row,
@@ -15,7 +17,9 @@ use tui::{
15
17
16use super::{align_view, push_jump, Align, Context, Editor, Open};
18use super::{align_view, push_jump, Align, Context, Editor, Open};
17
19
20+use helix_core::{
18−use helix_core::{path, text_annotations::InlineAnnotation, Selection};
21+ path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
22+};
19use helix_view::{
23use helix_view::{
20 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
24 document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
21 editor::Action,
25 editor::Action,
@@ -25,6 +29,7 @@ use helix_view::{
25
29
26use crate::{
30use crate::{
27 compositor::{self, Compositor},
31 compositor::{self, Compositor},
32+ job::Callback,
28 ui::{
33 ui::{
29 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
34 self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
30 Popup, PromptEvent,
35 Popup, PromptEvent,
@@ -35,24 +40,6 @@ use std::{
35 cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
40 cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
36};
41};
37
42
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−
56impl ui::menu::Item for lsp::Location {
43impl ui::menu::Item for lsp::Location {
57 /// Current working directory.
44 /// Current working directory.
58 type Data = PathBuf;
45 type Data = PathBuf;
@@ -87,20 +74,30 @@ impl ui::menu::Item for lsp::Location {
87 }
74 }
88}
75}
89
76
77+struct SymbolInformationItem {
78+ symbol: lsp::SymbolInformation,
79+ offset_encoding: OffsetEncoding,
80+}
81+
90−impl ui::menu::Item for lsp::SymbolInformation {
82+impl ui::menu::Item for SymbolInformationItem {
91 /// Path to currently focussed document
83 /// Path to currently focussed document
92 type Data = Option<lsp::Url>;
84 type Data = Option<lsp::Url>;
93
85
94 fn format(&self, current_doc_path: &Self::Data) -> Row {
86 fn format(&self, current_doc_path: &Self::Data) -> Row {
95− if current_doc_path.as_ref() == Some(&self.location.uri) {
87+ if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
96− self.name.as_str().into()
88+ self.symbol.name.as_str().into()
97 } else {
89 } else {
98− match self.location.uri.to_file_path() {
90+ match self.symbol.location.uri.to_file_path() {
99 Ok(path) => {
91 Ok(path) => {
100 let get_relative_path = path::get_relative_path(path.as_path());
92 let get_relative_path = path::get_relative_path(path.as_path());
93+ format!(
94+ "{} ({})",
95+ &self.symbol.name,
101− format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
96+ get_relative_path.to_string_lossy()
97+ )
98+ .into()
102 }
99 }
103− Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
100+ Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
104 }
101 }
105 }
102 }
106 }
103 }
@@ -116,6 +113,7 @@ struct DiagnosticStyles {
116struct PickerDiagnostic {
113struct PickerDiagnostic {
117 url: lsp::Url,
114 url: lsp::Url,
118 diag: lsp::Diagnostic,
115 diag: lsp::Diagnostic,
116+ offset_encoding: OffsetEncoding,
119}
117}
120
118
121impl ui::menu::Item for PickerDiagnostic {
119impl ui::menu::Item for PickerDiagnostic {
@@ -211,21 +209,19 @@ fn jump_to_location(
211 align_view(doc, view, Align::Center);
209 align_view(doc, view, Align::Center);
212}
210}
213
211
214−fn sym_picker(
212+type SymbolPicker = FilePicker<SymbolInformationItem>;
215− symbols: Vec<lsp::SymbolInformation>,
213+
216− current_path: Option<lsp::Url>,
214+fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
217− offset_encoding: OffsetEncoding,
218−) -> FilePicker<lsp::SymbolInformation> {
219 // TODO: drop current_path comparison and instead use workspace: bool flag?
215 // TODO: drop current_path comparison and instead use workspace: bool flag?
220 FilePicker::new(
216 FilePicker::new(
221 symbols,
217 symbols,
222 current_path.clone(),
218 current_path.clone(),
223− move |cx, symbol, action| {
219+ move |cx, item, action| {
224 let (view, doc) = current!(cx.editor);
220 let (view, doc) = current!(cx.editor);
225 push_jump(view, doc);
221 push_jump(view, doc);
226
222
227− if current_path.as_ref() != Some(&symbol.location.uri) {
223+ if current_path.as_ref() != Some(&item.symbol.location.uri) {
228− let uri = &symbol.location.uri;
224+ let uri = &item.symbol.location.uri;
229 let path = match uri.to_file_path() {
225 let path = match uri.to_file_path() {
230 Ok(path) => path,
226 Ok(path) => path,
231 Err(_) => {
227 Err(_) => {
@@ -245,7 +241,7 @@ fn sym_picker(
245 let (view, doc) = current!(cx.editor);
241 let (view, doc) = current!(cx.editor);
246
242
247 if let Some(range) =
243 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)
249 {
245 {
250 // we flip the range so that the cursor sits on the start of the symbol
246 // we flip the range so that the cursor sits on the start of the symbol
251 // (for example start of the function).
247 // (for example start of the function).
@@ -253,7 +249,7 @@ fn sym_picker(
253 align_view(doc, view, Align::Center);
249 align_view(doc, view, Align::Center);
254 }
250 }
255 },
251 },
256− move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
252+ move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
257 )
253 )
258 .truncate_start(false)
254 .truncate_start(false)
259}
255}
@@ -266,10 +262,9 @@ enum DiagnosticsFormat {
266
262
267fn diag_picker(
263fn diag_picker(
268 cx: &Context,
264 cx: &Context,
269− diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
265+ diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>,
270 current_path: Option<lsp::Url>,
266 current_path: Option<lsp::Url>,
271 format: DiagnosticsFormat,
267 format: DiagnosticsFormat,
272− offset_encoding: OffsetEncoding,
273) -> FilePicker<PickerDiagnostic> {
268) -> FilePicker<PickerDiagnostic> {
274 // TODO: drop current_path comparison and instead use workspace: bool flag?
269 // TODO: drop current_path comparison and instead use workspace: bool flag?
275
270
@@ -277,10 +272,11 @@ fn diag_picker(
277 let mut flat_diag = Vec::new();
272 let mut flat_diag = Vec::new();
278 for (url, diags) in diagnostics {
273 for (url, diags) in diagnostics {
279 flat_diag.reserve(diags.len());
274 flat_diag.reserve(diags.len());
280− for diag in diags {
275+ for (diag, _, offset_encoding) in diags {
281 flat_diag.push(PickerDiagnostic {
276 flat_diag.push(PickerDiagnostic {
282 url: url.clone(),
277 url: url.clone(),
283 diag,
278 diag,
279+ offset_encoding,
284 });
280 });
285 }
281 }
286 }
282 }
@@ -295,7 +291,13 @@ fn diag_picker(
295 FilePicker::new(
291 FilePicker::new(
296 flat_diag,
292 flat_diag,
297 (styles, format),
293 (styles, format),
294+ move |cx,
298− move |cx, PickerDiagnostic { url, diag }, action| {
295+ PickerDiagnostic {
296+ url,
297+ diag,
298+ offset_encoding,
299+ },
300+ action| {
299 if current_path.as_ref() == Some(url) {
301 if current_path.as_ref() == Some(url) {
300 let (view, doc) = current!(cx.editor);
302 let (view, doc) = current!(cx.editor);
301 push_jump(view, doc);
303 push_jump(view, doc);
@@ -306,14 +308,14 @@ fn diag_picker(
306
308
307 let (view, doc) = current!(cx.editor);
309 let (view, doc) = current!(cx.editor);
308
310
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) {
310 // we flip the range so that the cursor sits on the start of the symbol
312 // we flip the range so that the cursor sits on the start of the symbol
311 // (for example start of the function).
313 // (for example start of the function).
312 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
314 doc.set_selection(view.id, Selection::single(range.head, range.anchor));
313 align_view(doc, view, Align::Center);
315 align_view(doc, view, Align::Center);
314 }
316 }
315 },
317 },
316− move |_editor, PickerDiagnostic { url, diag }| {
318+ move |_editor, PickerDiagnostic { url, diag, .. }| {
317 let location = lsp::Location::new(url.clone(), diag.range);
319 let location = lsp::Location::new(url.clone(), diag.range);
318 Some(location_to_file_location(&location))
320 Some(location_to_file_location(&location))
319 },
321 },
@@ -323,126 +325,149 @@ fn diag_picker(
323
325
324pub fn symbol_picker(cx: &mut Context) {
326pub fn symbol_picker(cx: &mut Context) {
325 fn nested_to_flat(
327 fn nested_to_flat(
326− list: &mut Vec<lsp::SymbolInformation>,
328+ list: &mut Vec<SymbolInformationItem>,
327 file: &lsp::TextDocumentIdentifier,
329 file: &lsp::TextDocumentIdentifier,
328 symbol: lsp::DocumentSymbol,
330 symbol: lsp::DocumentSymbol,
331+ offset_encoding: OffsetEncoding,
329 ) {
332 ) {
330 #[allow(deprecated)]
333 #[allow(deprecated)]
334+ list.push(SymbolInformationItem {
331− list.push(lsp::SymbolInformation {
335+ symbol: lsp::SymbolInformation {
332− name: symbol.name,
336+ name: symbol.name,
333− kind: symbol.kind,
337+ kind: symbol.kind,
334− tags: symbol.tags,
338+ tags: symbol.tags,
335− deprecated: symbol.deprecated,
339+ deprecated: symbol.deprecated,
336− location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
340+ location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
337− container_name: None,
341+ container_name: None,
342+ },
343+ offset_encoding,
338 });
344 });
339 for child in symbol.children.into_iter().flatten() {
345 for child in symbol.children.into_iter().flatten() {
340− nested_to_flat(list, file, child);
346+ nested_to_flat(list, file, child, offset_encoding);
341 }
347 }
342 }
348 }
343 let doc = doc!(cx.editor);
349 let doc = doc!(cx.editor);
344
350
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();
346 let current_url = doc.url();
386 let current_url = doc.url();
347− let offset_encoding = language_server.offset_encoding();
348
387
349− let future = match language_server.document_symbols(doc.identifier()) {
388+ if futures.is_empty() {
350− Some(future) => future,
351− None => {
352− cx.editor
389+ cx.editor
353− .set_error("Language server does not support document symbols");
390+ .set_error("No Language server does support document symbols");
354− return;
391+ 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− }
392+ }
372− flat_symbols
373− }
374− };
375
393
376− let picker = sym_picker(symbols, current_url, offset_encoding);
394+ cx.jobs.callback(async move {
377− compositor.push(Box::new(overlaid(picker)))
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;
378 }
404 }
379− },
405+ let picker = sym_picker(symbols, current_url);
406+ compositor.push(Box::new(overlaid(picker)))
407+ };
408+
409+ Ok(Callback::EditorCompositor(Box::new(call)))
380− )
410+ });
381}
411}
382
412
383pub fn workspace_symbol_picker(cx: &mut Context) {
413pub fn workspace_symbol_picker(cx: &mut Context) {
384 let doc = doc!(cx.editor);
414 let doc = doc!(cx.editor);
385− let current_url = doc.url();
415+
386− let language_server = language_server!(cx.editor, doc);
416+ let get_symbols = move |pattern: String, editor: &mut Editor| {
387− let offset_encoding = language_server.offset_encoding();
417+ let doc = doc!(editor);
388− let future = match language_server.workspace_symbols("".to_string()) {
418+ let mut futures: FuturesUnordered<_> = doc
389− Some(future) => future,
419+ .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
390− None => {
420+ .iter()
391− cx.editor
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() {
392− .set_error("Language server does not support workspace symbols");
439+ editor.set_error("No Language server does support workspace symbols");
393− return;
394 }
440 }
395− };
396
441
397− cx.callback(
442+ async move {
398− future,
443+ let mut symbols = Vec::new();
399− move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
444+ // TODO if one symbol request errors, all other requests are discarded (even if they're valid)
400− let symbols = response.unwrap_or_default();
445+ while let Some(mut lsp_items) = futures.try_next().await? {
401− let picker = sym_picker(symbols, current_url, offset_encoding);
446+ symbols.append(&mut lsp_items);
402− let get_symbols = |query: String, editor: &mut Editor| {
403− let doc = doc!(editor);
404− let language_server = match doc.language_server() {
405− Some(s) => s,
406− None => {
407− // This should not generally happen since the picker will not
408− // even open in the first place if there is no server.
409− return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
410− }
447+ }
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!(
448+ anyhow::Ok(symbols)
419− "Language server does not support workspace symbols"
420− ))
421− }
449+ }
422− .boxed();
450+ .boxed()
423− }
424− };
451+ };
425
452
426− let future = async move {
453+ let current_url = doc.url();
427− let json = symbol_request.await?;
454+ let initial_symbols = get_symbols("".to_owned(), cx.editor);
428− let response: Option<Vec<lsp::SymbolInformation>> =
429− serde_json::from_value(json)?;
430
455
431− Ok(response.unwrap_or_default())
456+ cx.jobs.callback(async move {
432− };
457+ let symbols = initial_symbols.await?;
433− future.boxed()
458+ let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
434− };
459+ let picker = sym_picker(symbols, current_url);
435 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
460 let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
436 compositor.push(Box::new(overlaid(dyn_picker)))
461 compositor.push(Box::new(overlaid(dyn_picker)))
437− },
462+ };
463+
464+ Ok(Callback::EditorCompositor(Box::new(call)))
438− )
465+ });
439}
466}
440
467
441pub fn diagnostics_picker(cx: &mut Context) {
468pub fn diagnostics_picker(cx: &mut Context) {
442 let doc = doc!(cx.editor);
469 let doc = doc!(cx.editor);
443− let language_server = language_server!(cx.editor, doc);
444 if let Some(current_url) = doc.url() {
470 if let Some(current_url) = doc.url() {
445− let offset_encoding = language_server.offset_encoding();
446 let diagnostics = cx
471 let diagnostics = cx
447 .editor
472 .editor
448 .diagnostics
473 .diagnostics
@@ -454,7 +479,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
454 [(current_url.clone(), diagnostics)].into(),
479 [(current_url.clone(), diagnostics)].into(),
455 Some(current_url),
480 Some(current_url),
456 DiagnosticsFormat::HideSourcePath,
481 DiagnosticsFormat::HideSourcePath,
457− offset_encoding,
458 );
482 );
459 cx.push_layer(Box::new(overlaid(picker)));
483 cx.push_layer(Box::new(overlaid(picker)));
460 }
484 }
@@ -462,24 +486,28 @@ pub fn diagnostics_picker(cx: &mut Context) {
462
486
463pub fn workspace_diagnostics_picker(cx: &mut Context) {
487pub fn workspace_diagnostics_picker(cx: &mut Context) {
464 let doc = doc!(cx.editor);
488 let doc = doc!(cx.editor);
465− let language_server = language_server!(cx.editor, doc);
466 let current_url = doc.url();
489 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
468 let diagnostics = cx.editor.diagnostics.clone();
491 let diagnostics = cx.editor.diagnostics.clone();
469 let picker = diag_picker(
492 let picker = diag_picker(
470 cx,
493 cx,
471 diagnostics,
494 diagnostics,
472 current_url,
495 current_url,
473 DiagnosticsFormat::ShowSourcePath,
496 DiagnosticsFormat::ShowSourcePath,
474− offset_encoding,
475 );
497 );
476 cx.push_layer(Box::new(overlaid(picker)));
498 cx.push_layer(Box::new(overlaid(picker)));
477}
499}
478
500
501+struct CodeActionOrCommandItem {
502+ lsp_item: lsp::CodeActionOrCommand,
503+ offset_encoding: OffsetEncoding,
504+ language_server_id: usize,
505+}
506+
479−impl ui::menu::Item for lsp::CodeActionOrCommand {
507+impl ui::menu::Item for CodeActionOrCommandItem {
480 type Data = ();
508 type Data = ();
481 fn format(&self, _data: &Self::Data) -> Row {
509 fn format(&self, _data: &Self::Data) -> Row {
482− match self {
510+ match &self.lsp_item {
483 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
511 lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
484 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
512 lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
485 }
513 }
@@ -546,45 +574,40 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
546pub fn code_action(cx: &mut Context) {
574pub fn code_action(cx: &mut Context) {
547 let (view, doc) = current!(cx.editor);
575 let (view, doc) = current!(cx.editor);
548
576
549− let language_server = language_server!(cx.editor, doc);
550−
551 let selection_range = doc.selection(view.id).primary();
577 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);
555
578
556− let future = match language_server.code_actions(
579+ let mut futures: FuturesUnordered<_> = doc
557− doc.identifier(),
580+ .language_servers_with_feature(LanguageServerFeature::CodeAction)
558− range,
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);
559− // Filter and convert overlapping diagnostics
587+ // Filter and convert overlapping diagnostics
560− lsp::CodeActionContext {
588+ let code_action_context = lsp::CodeActionContext {
561− diagnostics: doc
589+ diagnostics: doc
562− .diagnostics()
590+ .diagnostics()
563− .iter()
591+ .iter()
564− .filter(|&diag| {
592+ .filter(|&diag| {
565− selection_range
593+ selection_range
566− .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
594+ .overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
567− })
595+ })
568− .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
596+ .map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
569− .collect(),
597+ .collect(),
570− only: None,
598+ only: None,
571− trigger_kind: Some(CodeActionTriggerKind::INVOKED),
599+ trigger_kind: Some(CodeActionTriggerKind::INVOKED),
572− },
600+ };
573− ) {
601+ let code_action_request =
574− Some(future) => future,
602+ language_server.code_actions(doc.identifier(), range, code_action_context)?;
575− None => {
603+ Some((code_action_request, offset_encoding, language_server_id))
576− cx.editor
577− .set_error("Language server does not support code actions");
578− return;
579− }
604+ })
580− };
605+ .map(|(request, offset_encoding, ls_id)| async move {
581−
606+ let json = request.await?;
582− cx.callback(
583− future,
584− move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
607+ let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
585 let mut actions = match response {
608 let mut actions = match response {
586 Some(a) => a,
609 Some(a) => a,
587− None => return,
610+ None => return anyhow::Ok(Vec::new()),
588 };
611 };
589
612
590 // remove disabled code actions
613 // remove disabled code actions
@@ -596,11 +619,6 @@ pub fn code_action(cx: &mut Context) {
596 )
619 )
597 });
620 });
598
621
599− if actions.is_empty() {
600− editor.set_status("No code actions available");
601− return;
602− }
603−
604 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
622 // Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
605 // Many details are modeled after vscode because language servers are usually tested against it.
623 // Many details are modeled after vscode because language servers are usually tested against it.
606 // VScode sorts the codeaction two times:
624 // VScode sorts the codeaction two times:
@@ -636,18 +654,48 @@ pub fn code_action(cx: &mut Context) {
636 .reverse()
654 .reverse()
637 });
655 });
638
656
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+ }
639− let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
686+ let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
640 if event != PromptEvent::Validate {
687 if event != PromptEvent::Validate {
641 return;
688 return;
642 }
689 }
643
690
644 // always present here
691 // always present here
645− let code_action = code_action.unwrap();
692+ let action = action.unwrap();
693+ let offset_encoding = action.offset_encoding;
646
694
647− match code_action {
695+ match &action.lsp_item {
648 lsp::CodeActionOrCommand::Command(command) => {
696 lsp::CodeActionOrCommand::Command(command) => {
649 log::debug!("code action command: {:?}", command);
697 log::debug!("code action command: {:?}", command);
650− execute_lsp_command(editor, command.clone());
698+ execute_lsp_command(editor, action.language_server_id, command.clone());
651 }
699 }
652 lsp::CodeActionOrCommand::CodeAction(code_action) => {
700 lsp::CodeActionOrCommand::CodeAction(code_action) => {
653 log::debug!("code action: {:?}", code_action);
701 log::debug!("code action: {:?}", code_action);
@@ -659,7 +707,7 @@ pub fn code_action(cx: &mut Context) {
659 // if code action provides both edit and command first the edit
707 // if code action provides both edit and command first the edit
660 // should be applied and then the command
708 // should be applied and then the command
661 if let Some(command) = &code_action.command {
709 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());
663 }
711 }
664 }
712 }
665 }
713 }
@@ -668,8 +716,10 @@ pub fn code_action(cx: &mut Context) {
668
716
669 let popup = Popup::new("code-action", picker).with_scrollbar(false);
717 let popup = Popup::new("code-action", picker).with_scrollbar(false);
670 compositor.replace_or_push("code-action", popup);
718 compositor.replace_or_push("code-action", popup);
671− },
719+ };
720+
721+ Ok(Callback::EditorCompositor(Box::new(call)))
672− )
722+ });
673}
723}
674
724
675impl ui::menu::Item for lsp::Command {
725impl ui::menu::Item for lsp::Command {
@@ -679,13 +729,14 @@ impl ui::menu::Item for lsp::Command {
679 }
729 }
680}
730}
681
731
682−pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
732+pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
683− let doc = doc!(editor);
684− let language_server = language_server!(editor, doc);
685−
686 // the command is executed on the server and communicated back
733 // the command is executed on the server and communicated back
687 // to the client asynchronously using workspace edits
734 // to the client asynchronously using workspace edits
735+ let future = match editor
736+ .language_servers
737+ .get_by_id(language_server_id)
688− let future = match language_server.command(cmd) {
738+ .and_then(|language_server| language_server.command(cmd))
739+ {
689 Some(future) => future,
740 Some(future) => future,
690 None => {
741 None => {
691 editor.set_error("Language server does not support executing commands");
742 editor.set_error("Language server does not support executing commands");
@@ -977,18 +1028,22 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
977 }
1028 }
978}
1029}
979
1030
1031+// TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity...
980pub fn goto_declaration(cx: &mut Context) {
1032pub fn goto_declaration(cx: &mut Context) {
981 let (view, doc) = current!(cx.editor);
1033 let (view, doc) = current!(cx.editor);
982− let language_server = language_server!(cx.editor, doc);
1034+ let (future, offset_encoding) = match doc
1035+ .language_servers_with_feature(LanguageServerFeature::GotoDeclaration)
1036+ .iter()
1037+ .find_map(|language_server| {
983− let offset_encoding = language_server.offset_encoding();
1038+ let offset_encoding = language_server.offset_encoding();
984−
985− let pos = doc.position(view.id, offset_encoding);
1039+ let pos = doc.position(view.id, offset_encoding);
986−
987− let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
1040+ let future = language_server.goto_declaration(doc.identifier(), pos, None)?;
988− Some(future) => future,
1041+ Some((future, offset_encoding))
1042+ }) {
1043+ Some(future_offset_encoding) => future_offset_encoding,
989 None => {
1044 None => {
990 cx.editor
1045 cx.editor
991− .set_error("Language server does not support goto-declaration");
1046+ .set_error("No language server supports goto-declaration");
992 return;
1047 return;
993 }
1048 }
994 };
1049 };
@@ -1004,16 +1059,19 @@ pub fn goto_declaration(cx: &mut Context) {
1004
1059
1005pub fn goto_definition(cx: &mut Context) {
1060pub fn goto_definition(cx: &mut Context) {
1006 let (view, doc) = current!(cx.editor);
1061 let (view, doc) = current!(cx.editor);
1007− let language_server = language_server!(cx.editor, doc);
1062+ let (future, offset_encoding) = match doc
1063+ .language_servers_with_feature(LanguageServerFeature::GotoDefinition)
1064+ .iter()
1065+ .find_map(|language_server| {
1008− let offset_encoding = language_server.offset_encoding();
1066+ let offset_encoding = language_server.offset_encoding();
1009−
1010− let pos = doc.position(view.id, offset_encoding);
1067+ let pos = doc.position(view.id, offset_encoding);
1011−
1012− let future = match language_server.goto_definition(doc.identifier(), pos, None) {
1068+ let future = language_server.goto_definition(doc.identifier(), pos, None)?;
1013− Some(future) => future,
1069+ Some((future, offset_encoding))
1070+ }) {
1071+ Some(future_offset_encoding) => future_offset_encoding,
1014 None => {
1072 None => {
1015 cx.editor
1073 cx.editor
1016− .set_error("Language server does not support goto-definition");
1074+ .set_error("No language server supports goto-definition");
1017 return;
1075 return;
1018 }
1076 }
1019 };
1077 };
@@ -1029,16 +1087,19 @@ pub fn goto_definition(cx: &mut Context) {
1029
1087
1030pub fn goto_type_definition(cx: &mut Context) {
1088pub fn goto_type_definition(cx: &mut Context) {
1031 let (view, doc) = current!(cx.editor);
1089 let (view, doc) = current!(cx.editor);
1032− let language_server = language_server!(cx.editor, doc);
1090+ let (future, offset_encoding) = match doc
1091+ .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition)
1092+ .iter()
1093+ .find_map(|language_server| {
1033− let offset_encoding = language_server.offset_encoding();
1094+ let offset_encoding = language_server.offset_encoding();
1034−
1035− let pos = doc.position(view.id, offset_encoding);
1095+ let pos = doc.position(view.id, offset_encoding);
1036−
1037− let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
1096+ let future = language_server.goto_type_definition(doc.identifier(), pos, None)?;
1038− Some(future) => future,
1097+ Some((future, offset_encoding))
1098+ }) {
1099+ Some(future_offset_encoding) => future_offset_encoding,
1039 None => {
1100 None => {
1040 cx.editor
1101 cx.editor
1041− .set_error("Language server does not support goto-type-definition");
1102+ .set_error("No language server supports goto-type-definition");
1042 return;
1103 return;
1043 }
1104 }
1044 };
1105 };
@@ -1054,16 +1115,19 @@ pub fn goto_type_definition(cx: &mut Context) {
1054
1115
1055pub fn goto_implementation(cx: &mut Context) {
1116pub fn goto_implementation(cx: &mut Context) {
1056 let (view, doc) = current!(cx.editor);
1117 let (view, doc) = current!(cx.editor);
1057− let language_server = language_server!(cx.editor, doc);
1118+ let (future, offset_encoding) = match doc
1119+ .language_servers_with_feature(LanguageServerFeature::GotoImplementation)
1120+ .iter()
1121+ .find_map(|language_server| {
1058− let offset_encoding = language_server.offset_encoding();
1122+ let offset_encoding = language_server.offset_encoding();
1059−
1060− let pos = doc.position(view.id, offset_encoding);
1123+ let pos = doc.position(view.id, offset_encoding);
1061−
1062− let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
1124+ let future = language_server.goto_implementation(doc.identifier(), pos, None)?;
1063− Some(future) => future,
1125+ Some((future, offset_encoding))
1126+ }) {
1127+ Some(future_offset_encoding) => future_offset_encoding,
1064 None => {
1128 None => {
1065 cx.editor
1129 cx.editor
1066− .set_error("Language server does not support goto-implementation");
1130+ .set_error("no language server supports goto-implementation");
1067 return;
1131 return;
1068 }
1132 }
1069 };
1133 };
@@ -1080,21 +1144,24 @@ pub fn goto_implementation(cx: &mut Context) {
1080pub fn goto_reference(cx: &mut Context) {
1144pub fn goto_reference(cx: &mut Context) {
1081 let config = cx.editor.config();
1145 let config = cx.editor.config();
1082 let (view, doc) = current!(cx.editor);
1146 let (view, doc) = current!(cx.editor);
1083− let language_server = language_server!(cx.editor, doc);
1147+ let (future, offset_encoding) = match doc
1148+ .language_servers_with_feature(LanguageServerFeature::GotoReference)
1149+ .iter()
1150+ .find_map(|language_server| {
1084− let offset_encoding = language_server.offset_encoding();
1151+ let offset_encoding = language_server.offset_encoding();
1085−
1086− let pos = doc.position(view.id, offset_encoding);
1152+ let pos = doc.position(view.id, offset_encoding);
1087−
1088− let future = match language_server.goto_reference(
1153+ let future = language_server.goto_reference(
1089− doc.identifier(),
1154+ doc.identifier(),
1090− pos,
1155+ pos,
1091− config.lsp.goto_reference_include_declaration,
1156+ config.lsp.goto_reference_include_declaration,
1092− None,
1157+ None,
1093− ) {
1158+ )?;
1094− Some(future) => future,
1159+ Some((future, offset_encoding))
1160+ }) {
1161+ Some(future_offset_encoding) => future_offset_encoding,
1095 None => {
1162 None => {
1096 cx.editor
1163 cx.editor
1097− .set_error("Language server does not support goto-reference");
1164+ .set_error("language server supports goto-reference");
1098 return;
1165 return;
1099 }
1166 }
1100 };
1167 };
@@ -1108,7 +1175,7 @@ pub fn goto_reference(cx: &mut Context) {
1108 );
1175 );
1109}
1176}
1110
1177
1111−#[derive(PartialEq, Eq)]
1178+#[derive(PartialEq, Eq, Clone, Copy)]
1112pub enum SignatureHelpInvoked {
1179pub enum SignatureHelpInvoked {
1113 Manual,
1180 Manual,
1114 Automatic,
1181 Automatic,
@@ -1120,35 +1187,34 @@ pub fn signature_help(cx: &mut Context) {
1120
1187
1121pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1188pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1122 let (view, doc) = current!(cx.editor);
1189 let (view, doc) = current!(cx.editor);
1123− let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
1124
1190
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()
1125− let language_server = match doc.language_server() {
1195+ .find_map(|language_server| {
1126− Some(language_server) => 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(),
1127 None => {
1200 None => {
1128 // Do not show the message if signature help was invoked
1201 // Do not show the message if signature help was invoked
1129 // automatically on backspace, trigger characters, etc.
1202 // automatically on backspace, trigger characters, etc.
1130− if was_manually_invoked {
1203+ if invoked == SignatureHelpInvoked::Manual {
1131 cx.editor
1204 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");
1147 }
1206 }
1148 return;
1207 return;
1149 }
1208 }
1150 };
1209 };
1210+ signature_help_impl_with_future(cx, future, invoked);
1211+}
1151
1212
1213+pub fn signature_help_impl_with_future(
1214+ cx: &mut Context,
1215+ future: BoxFuture<'static, helix_lsp::Result<Value>>,
1216+ invoked: SignatureHelpInvoked,
1217+) {
1152 cx.callback(
1218 cx.callback(
1153 future,
1219 future,
1154 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
1220 move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@@ -1156,7 +1222,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1156
1222
1157 if !(config.lsp.auto_signature_help
1223 if !(config.lsp.auto_signature_help
1158 || SignatureHelp::visible_popup(compositor).is_some()
1224 || SignatureHelp::visible_popup(compositor).is_some()
1159− || was_manually_invoked)
1225+ || invoked == SignatureHelpInvoked::Manual)
1160 {
1226 {
1161 return;
1227 return;
1162 }
1228 }
@@ -1165,7 +1231,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1165 // it very probably means the server was a little slow to respond and the user has
1231 // it very probably means the server was a little slow to respond and the user has
1166 // already moved on to something else, making a signature help popup will just be an
1232 // already moved on to something else, making a signature help popup will just be an
1167 // annoyance, see https://github.com/helix-editor/helix/issues/3112
1233 // 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 {
1169 return;
1235 return;
1170 }
1236 }
1171
1237
@@ -1255,18 +1321,20 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
1255
1321
1256pub fn hover(cx: &mut Context) {
1322pub fn hover(cx: &mut Context) {
1257 let (view, doc) = current!(cx.editor);
1323 let (view, doc) = current!(cx.editor);
1258− let language_server = language_server!(cx.editor, doc);
1259− let offset_encoding = language_server.offset_encoding();
1260
1324
1261 // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
1325 // 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+ });
1262
1333
1263− let pos = doc.position(view.id, offset_encoding);
1334+ let future = match request {
1264−
1265− let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
1266 Some(future) => future,
1335 Some(future) => future,
1267 None => {
1336 None => {
1268− cx.editor
1269− .set_error("Language server does not support hover");
1337+ cx.editor.set_error("No language server supports hover");
1270 return;
1338 return;
1271 }
1339 }
1272 };
1340 };
@@ -1349,7 +1417,11 @@ pub fn rename_symbol(cx: &mut Context) {
1349 }
1417 }
1350 }
1418 }
1351
1419
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> {
1353 let prompt = ui::Prompt::new(
1425 let prompt = ui::Prompt::new(
1354 "rename-to:".into(),
1426 "rename-to:".into(),
1355 None,
1427 None,
@@ -1358,27 +1430,36 @@ pub fn rename_symbol(cx: &mut Context) {
1358 if event != PromptEvent::Validate {
1430 if event != PromptEvent::Validate {
1359 return;
1431 return;
1360 }
1432 }
1361−
1362 let (view, doc) = current!(cx.editor);
1433 let (view, doc) = current!(cx.editor);
1363− let language_server = language_server!(cx.editor, doc);
1434+ let request = doc
1364− let offset_encoding = language_server.offset_encoding();
1435+ .language_servers_with_feature(LanguageServerFeature::RenameSymbol)
1365−
1436+ .iter()
1366− let pos = doc.position(view.id, offset_encoding);
1437+ .find_map(|language_server| {
1367−
1438+ if let Some(language_server_id) = language_server_id {
1368− let future =
1439+ if language_server.id() != language_server_id {
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;
1440+ return None;
1441+ }
1375 }
1442 }
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))
1376− };
1451+ });
1452+
1453+ if let Some((future, offset_encoding)) = request {
1377− match block_on(future) {
1454+ match block_on(future) {
1378− Ok(edits) => {
1455+ Ok(edits) => {
1379− let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
1456+ let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
1457+ }
1458+ Err(err) => cx.editor.set_error(err.to_string()),
1380 }
1459 }
1381− Err(err) => cx.editor.set_error(err.to_string()),
1460+ } else {
1461+ cx.editor
1462+ .set_error("No language server supports symbol renaming");
1382 }
1463 }
1383 },
1464 },
1384 )
1465 )
@@ -1388,20 +1469,20 @@ pub fn rename_symbol(cx: &mut Context) {
1388 }
1469 }
1389
1470
1390 let (view, doc) = current!(cx.editor);
1471 let (view, doc) = current!(cx.editor);
1391− let language_server = language_server!(cx.editor, doc);
1392− let offset_encoding = language_server.offset_encoding();
1393−
1394− if !language_server.supports_rename() {
1395− cx.editor
1396− .set_error("Language server does not support symbol renaming");
1397− return;
1398− }
1399
1472
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();
1400− let pos = doc.position(view.id, 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+ });
1401
1482
1402− match language_server.prepare_rename(doc.identifier(), pos) {
1483+ match prepare_rename_request {
1403 // Language server supports textDocument/prepareRename, use it.
1484 // Language server supports textDocument/prepareRename, use it.
1404− Some(future) => cx.callback(
1485+ Some((future, offset_encoding, ls_id)) => cx.callback(
1405 future,
1486 future,
1406 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
1487 move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
1407 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
1488 let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@@ -1413,7 +1494,7 @@ pub fn rename_symbol(cx: &mut Context) {
1413 }
1494 }
1414 };
1495 };
1415
1496
1416− let prompt = create_rename_prompt(editor, prefill);
1497+ let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
1417
1498
1418 compositor.push(prompt);
1499 compositor.push(prompt);
1419 },
1500 },
@@ -1423,7 +1504,7 @@ pub fn rename_symbol(cx: &mut Context) {
1423 None => {
1504 None => {
1424 let prefill = get_prefill_from_word_boundary(cx.editor);
1505 let prefill = get_prefill_from_word_boundary(cx.editor);
1425
1506
1426− let prompt = create_rename_prompt(cx.editor, prefill);
1507+ let prompt = create_rename_prompt(cx.editor, prefill, None);
1427
1508
1428 cx.push_layer(prompt);
1509 cx.push_layer(prompt);
1429 }
1510 }
@@ -1432,17 +1513,20 @@ pub fn rename_symbol(cx: &mut Context) {
1432
1513
1433pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1514pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1434 let (view, doc) = current!(cx.editor);
1515 let (view, doc) = current!(cx.editor);
1435− let language_server = language_server!(cx.editor, doc);
1516+ let (future, offset_encoding) = match doc
1517+ .language_servers_with_feature(LanguageServerFeature::DocumentHighlight)
1518+ .iter()
1519+ .find_map(|language_server| {
1436− let offset_encoding = language_server.offset_encoding();
1520+ let offset_encoding = language_server.offset_encoding();
1437−
1438− let pos = doc.position(view.id, offset_encoding);
1521+ let pos = doc.position(view.id, offset_encoding);
1439−
1522+ let future =
1440− let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
1523+ language_server.text_document_document_highlight(doc.identifier(), pos, None)?;
1524+ Some((future, offset_encoding))
1441− {
1525+ }) {
1442 Some(future) => future,
1526 Some(future) => future,
1443 None => {
1527 None => {
1444 cx.editor
1528 cx.editor
1445− .set_error("Language server does not support document highlight");
1529+ .set_error("No language server supports document-highlight");
1446 return;
1530 return;
1447 }
1531 }
1448 };
1532 };
@@ -1455,8 +1539,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
1455 _ => return,
1539 _ => return,
1456 };
1540 };
1457 let (view, doc) = current!(editor);
1541 let (view, doc) = current!(editor);
1458− let language_server = language_server!(editor, doc);
1459− let offset_encoding = language_server.offset_encoding();
1460 let text = doc.text();
1542 let text = doc.text();
1461 let pos = doc.selection(view.id).primary().head;
1543 let pos = doc.selection(view.id).primary().head;
1462
1544
@@ -1502,63 +1584,58 @@ fn compute_inlay_hints_for_view(
1502 let view_id = view.id;
1584 let view_id = view.id;
1503 let doc_id = view.doc;
1585 let doc_id = view.doc;
1504
1586
1505− let language_server = doc.language_server()?;
1587+ let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints);
1506−
1588+ let language_server = language_servers.iter().find(|language_server| {
1507− let capabilities = language_server.capabilities();
1589+ matches!(
1508−
1590+ language_server.capabilities().inlay_hint_provider,
1509− let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
1510− Some(
1591+ Some(
1511− lsp::OneOf::Left(true)
1592+ lsp::OneOf::Left(true)
1512− | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
1593+ | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_))
1594+ )
1513− ) => {
1595+ )
1596+ })?;
1597+
1514− let doc_text = doc.text();
1598+ let doc_text = doc.text();
1515− let len_lines = doc_text.len_lines();
1599+ let len_lines = doc_text.len_lines();
1516−
1600+
1517− // Compute ~3 times the current view height of inlay hints, that way some scrolling
1601+ // 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
1602+ // 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
1603+ // than computing all the hints for the full file (which could be dozens of time
1520− // longer than the view is).
1604+ // longer than the view is).
1521− let view_height = view.inner_height();
1605+ 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()));
1606+ let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
1524− let first_line = first_visible_line.saturating_sub(view_height);
1607+ let first_line = first_visible_line.saturating_sub(view_height);
1525− let last_line = first_visible_line
1608+ let last_line = first_visible_line
1526− .saturating_add(view_height.saturating_mul(2))
1609+ .saturating_add(view_height.saturating_mul(2))
1527− .min(len_lines);
1610+ .min(len_lines);
1528−
1611+
1529− let new_doc_inlay_hint_id = DocumentInlayHintsId {
1612+ let new_doc_inlay_hints_id = DocumentInlayHintsId {
1530− first_line,
1613+ first_line,
1531− last_line,
1614+ last_line,
1532− };
1615+ };
1533− // Don't recompute the annotations in case nothing has changed about the view
1616+ // Don't recompute the annotations in case nothing has changed about the view
1534− if !doc.inlay_hints_oudated
1617+ if !doc.inlay_hints_oudated
1535− && doc
1618+ && doc
1536− .inlay_hints(view_id)
1619+ .inlay_hints(view_id)
1537− .map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
1620+ .map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
1538− {
1621+ {
1539− return None;
1622+ return None;
1540− }
1623+ }
1541
1624
1542− let doc_slice = doc_text.slice(..);
1625+ let doc_slice = doc_text.slice(..);
1543− let first_char_in_range = doc_slice.line_to_char(first_line);
1626+ 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);
1627+ let last_char_in_range = doc_slice.line_to_char(last_line);
1545
1628
1546− let range = helix_lsp::util::range_to_lsp_range(
1629+ let range = helix_lsp::util::range_to_lsp_range(
1547− doc_text,
1630+ doc_text,
1548− helix_core::Range::new(first_char_in_range, last_char_in_range),
1631+ helix_core::Range::new(first_char_in_range, last_char_in_range),
1549− language_server.offset_encoding(),
1632+ language_server.offset_encoding(),
1550− );
1633+ );
1551
1634
1552− (
1635+ let offset_encoding = language_server.offset_encoding();
1553− language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
1554− new_doc_inlay_hint_id,
1555− )
1556− }
1557− _ => return None,
1558− };
1559
1636
1560 let callback = super::make_job_callback(
1637 let callback = super::make_job_callback(
1561− future?,
1638+ language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
1562 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
1639 move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
1563 // The config was modified or the window was closed while the request was in flight
1640 // The config was modified or the window was closed while the request was in flight
1564 if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
1641 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(
1572 };
1649 };
1573
1650
1574 // If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
1651 // 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()) {
1652+ let mut hints = match response {
1576− (Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
1653+ Some(hints) if !hints.is_empty() => hints,
1577 _ => {
1654 _ => {
1578 doc.set_inlay_hints(
1655 doc.set_inlay_hints(
1579 view_id,
1656 view_id,
helix-term/src/commands/typed.rs+73−33
@@ -1329,23 +1329,20 @@ fn lsp_workspace_command(
1329 if event != PromptEvent::Validate {
1329 if event != PromptEvent::Validate {
1330 return Ok(());
1330 return Ok(());
1331 }
1331 }
1332−
1333− let (_, doc) = current!(cx.editor);
1332+ let doc = doc!(cx.editor);
1334−
1333+ let language_servers =
1335− let language_server = match doc.language_server() {
1334+ doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
1336− Some(language_server) => language_server,
1335+ let (language_server_id, options) = match language_servers.iter().find_map(|ls| {
1337− None => {
1336+ ls.capabilities()
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 {
1337+ .execute_command_provider
1345− Some(options) => options,
1338+ .as_ref()
1339+ .map(|options| (ls.id(), options))
1340+ }) {
1341+ Some(id_options) => id_options,
1346 None => {
1342 None => {
1347− cx.editor
1343+ cx.editor.set_status(
1348− .set_status("Workspace commands are not supported for this language server");
1344+ "No active language servers for this document support workspace commands",
1345+ );
1349 return Ok(());
1346 return Ok(());
1350 }
1347 }
1351 };
1348 };
@@ -1362,8 +1359,8 @@ fn lsp_workspace_command(
1362 let callback = async move {
1359 let callback = async move {
1363 let call: job::Callback = Callback::EditorCompositor(Box::new(
1360 let call: job::Callback = Callback::EditorCompositor(Box::new(
1364 move |_editor: &mut Editor, compositor: &mut Compositor| {
1361 move |_editor: &mut Editor, compositor: &mut Compositor| {
1365− let picker = ui::Picker::new(commands, (), |cx, command, _action| {
1362+ let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
1366− execute_lsp_command(cx.editor, command.clone());
1363+ execute_lsp_command(cx.editor, language_server_id, command.clone());
1367 });
1364 });
1368 compositor.push(Box::new(overlaid(picker)))
1365 compositor.push(Box::new(overlaid(picker)))
1369 },
1366 },
@@ -1376,6 +1373,7 @@ fn lsp_workspace_command(
1376 if options.commands.iter().any(|c| c == &command) {
1373 if options.commands.iter().any(|c| c == &command) {
1377 execute_lsp_command(
1374 execute_lsp_command(
1378 cx.editor,
1375 cx.editor,
1376+ language_server_id,
1379 helix_lsp::lsp::Command {
1377 helix_lsp::lsp::Command {
1380 title: command.clone(),
1378 title: command.clone(),
1381 arguments: None,
1379 arguments: None,
@@ -1426,7 +1424,7 @@ fn lsp_restart(
1426 .collect();
1424 .collect();
1427
1425
1428 for document_id in document_ids_to_refresh {
1426 for document_id in document_ids_to_refresh {
1429− cx.editor.refresh_language_server(document_id);
1427+ cx.editor.refresh_language_servers(document_id);
1430 }
1428 }
1431
1429
1432 Ok(())
1430 Ok(())
@@ -1443,21 +1441,63 @@ fn lsp_stop(
1443
1441
1444 let doc = doc!(cx.editor);
1442 let doc = doc!(cx.editor);
1445
1443
1446− let ls_id = doc
1444+ // TODO this stops language servers which may be used in another doc/language type that uses the same language servers
1447− .language_server()
1445+ // I'm not sure if this is really what we want
1446+ let ls_shutdown_names = doc
1447+ .language_servers()
1448+ .iter()
1448− .map(|ls| ls.id())
1449+ .map(|ls| ls.name())
1449− .context("LSP not running for the current document")?;
1450+ .collect::<Vec<_>>();
1450
1451
1451− let config = doc
1452+ for ls_name in &ls_shutdown_names {
1452− .language_config()
1453− .context("LSP not defined for the current document")?;
1454− cx.editor.language_servers.stop(config);
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
1455
1496
1456− for doc in cx.editor.documents_mut() {
1497+ for client_id in stopped_clients {
1457− if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
1498+ doc.clear_diagnostics(client_id)
1458− doc.set_language_server(None);
1459− doc.set_diagnostics(Default::default());
1460 }
1499 }
1500+ doc.set_language_servers(active_clients);
1461 }
1501 }
1462
1502
1463 Ok(())
1503 Ok(())
@@ -1850,7 +1890,7 @@ fn language(
1850 doc.detect_indent_and_line_ending();
1890 doc.detect_indent_and_line_ending();
1851
1891
1852 let id = doc.id();
1892 let id = doc.id();
1853− cx.editor.refresh_language_server(id);
1893+ cx.editor.refresh_language_servers(id);
1854 Ok(())
1894 Ok(())
1855}
1895}
1856
1896
@@ -2588,7 +2628,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
2588 TypableCommand {
2628 TypableCommand {
2589 name: "lsp-restart",
2629 name: "lsp-restart",
2590 aliases: &[],
2630 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",
2592 fun: lsp_restart,
2632 fun: lsp_restart,
2593 signature: CommandSignature::none(),
2633 signature: CommandSignature::none(),
2594 },
2634 },
helix-term/src/health.rs+19−8
@@ -2,7 +2,10 @@ use crossterm::{
2 style::{Color, Print, Stylize},
2 style::{Color, Print, Stylize},
3 tty::IsTty,
3 tty::IsTty,
4};
4};
5+use helix_core::{
5−use helix_core::config::{default_syntax_loader, user_syntax_loader};
6+ config::{default_syntax_loader, user_syntax_loader},
7+ syntax::LanguageServerFeatureConfiguration,
8+};
6use helix_loader::grammar::load_runtime_file;
9use helix_loader::grammar::load_runtime_file;
7use helix_view::clipboard::get_clipboard_provider;
10use helix_view::clipboard::get_clipboard_provider;
8use std::io::Write;
11use std::io::Write;
@@ -192,10 +195,14 @@ pub fn languages_all() -> std::io::Result<()> {
192 for lang in &syn_loader_conf.language {
195 for lang in &syn_loader_conf.language {
193 column(&lang.language_id, Color::Reset);
196 column(&lang.language_id, Color::Reset);
194
197
195− let lsp = lang
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
196− .language_server
202+ .language_server
197− .as_ref()
203+ .get(lsp.name())
198− .map(|lsp| lsp.command.to_string());
204+ .map(|config| config.command.clone())
205+ });
199 check_binary(lsp);
206 check_binary(lsp);
200
207
201 let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
208 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<()> {
264 }
271 }
265 };
272 };
266
273
274+ // TODO multiple language servers
267 probe_protocol(
275 probe_protocol(
268 "language server",
276 "language server",
277+ lang.language_servers.first().and_then(|lsp| {
278+ syn_loader_conf
269− lang.language_server
279+ .language_server
270− .as_ref()
280+ .get(lsp.name())
271− .map(|lsp| lsp.command.to_string()),
281+ .map(|config| config.command.clone())
282+ }),
272 )?;
283 )?;
273
284
274 probe_protocol(
285 probe_protocol(
helix-term/src/ui/completion.rs+42−43
@@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
15use crate::commands;
15use crate::commands;
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
16use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
17
17
18−use helix_lsp::{lsp, util};
18+use helix_lsp::{lsp, util, OffsetEncoding};
19
19
20impl menu::Item for CompletionItem {
20impl menu::Item for CompletionItem {
21 type Data = ();
21 type Data = ();
@@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
38 || self.item.tags.as_ref().map_or(false, |tags| {
38 || self.item.tags.as_ref().map_or(false, |tags| {
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
39 tags.contains(&lsp::CompletionItemTag::DEPRECATED)
40 });
40 });
41+
41 menu::Row::new(vec![
42 menu::Row::new(vec![
42 menu::Cell::from(Span::styled(
43 menu::Cell::from(Span::styled(
43 self.item.label.as_str(),
44 self.item.label.as_str(),
@@ -79,19 +80,16 @@ impl menu::Item for CompletionItem {
79 }
80 }
80 None => "",
81 None => "",
81 }),
82 }),
82− // self.detail.as_deref().unwrap_or("")
83− // self.label_details
84− // .as_ref()
85− // .or(self.detail())
86− // .as_str(),
87 ])
83 ])
88 }
84 }
89}
85}
90
86
91#[derive(Debug, PartialEq, Default, Clone)]
87#[derive(Debug, PartialEq, Default, Clone)]
92−struct CompletionItem {
88+pub struct CompletionItem {
93− item: lsp::CompletionItem,
89+ pub item: lsp::CompletionItem,
90+ pub language_server_id: usize,
91+ pub offset_encoding: OffsetEncoding,
94− resolved: bool,
92+ pub resolved: bool,
95}
93}
96
94
97/// Wraps a Menu.
95/// Wraps a Menu.
@@ -109,21 +107,13 @@ impl Completion {
109 pub fn new(
107 pub fn new(
110 editor: &Editor,
108 editor: &Editor,
111 savepoint: Arc<SavePoint>,
109 savepoint: Arc<SavePoint>,
112− mut items: Vec<lsp::CompletionItem>,
110+ mut items: Vec<CompletionItem>,
113− offset_encoding: helix_lsp::OffsetEncoding,
114 start_offset: usize,
111 start_offset: usize,
115 trigger_offset: usize,
112 trigger_offset: usize,
116 ) -> Self {
113 ) -> Self {
117 let replace_mode = editor.config().completion_replace;
114 let replace_mode = editor.config().completion_replace;
118 // Sort completion items according to their preselect status (given by the LSP server)
115 // Sort completion items according to their preselect status (given by the LSP server)
119− items.sort_by_key(|item| !item.preselect.unwrap_or(false));
116+ items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
120− let items = items
121− .into_iter()
122− .map(|item| CompletionItem {
123− item,
124− resolved: false,
125− })
126− .collect();
127
117
128 // Then create the menu
118 // Then create the menu
129 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
119 let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
@@ -131,7 +121,6 @@ impl Completion {
131 doc: &Document,
121 doc: &Document,
132 view_id: ViewId,
122 view_id: ViewId,
133 item: &CompletionItem,
123 item: &CompletionItem,
134− offset_encoding: helix_lsp::OffsetEncoding,
135 trigger_offset: usize,
124 trigger_offset: usize,
136 include_placeholder: bool,
125 include_placeholder: bool,
137 replace_mode: bool,
126 replace_mode: bool,
@@ -154,6 +143,8 @@ impl Completion {
154 }
143 }
155 };
144 };
156
145
146+ let offset_encoding = item.offset_encoding;
147+
157 let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
148 let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
158 return Transaction::new(doc.text());
149 return Transaction::new(doc.text());
159 };
150 };
@@ -247,15 +238,8 @@ impl Completion {
247 // always present here
238 // always present here
248 let item = item.unwrap();
239 let item = item.unwrap();
249
240
250− let transaction = item_to_transaction(
241+ let transaction =
251− doc,
242+ item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode);
252− view.id,
253− item,
254− offset_encoding,
255− trigger_offset,
256− true,
257− replace_mode,
258− );
259 doc.apply_temporary(&transaction, view.id);
243 doc.apply_temporary(&transaction, view.id);
260 }
244 }
261 PromptEvent::Validate => {
245 PromptEvent::Validate => {
@@ -267,10 +251,15 @@ impl Completion {
267 // always present here
251 // always present here
268 let mut item = item.unwrap().clone();
252 let mut item = item.unwrap().clone();
269
253
254+ let language_server = editor
255+ .language_servers
256+ .get_by_id(item.language_server_id)
257+ .unwrap();
258+
270 // resolve item if not yet resolved
259 // resolve item if not yet resolved
271 if !item.resolved {
260 if !item.resolved {
272 if let Some(resolved) =
261 if let Some(resolved) =
273− Self::resolve_completion_item(doc, item.item.clone())
262+ Self::resolve_completion_item(language_server, item.item.clone())
274 {
263 {
275 item.item = resolved;
264 item.item = resolved;
276 }
265 }
@@ -281,7 +270,6 @@ impl Completion {
281 doc,
270 doc,
282 view.id,
271 view.id,
283 &item,
272 &item,
284− offset_encoding,
285 trigger_offset,
273 trigger_offset,
286 false,
274 false,
287 replace_mode,
275 replace_mode,
@@ -299,7 +287,7 @@ impl Completion {
299 let transaction = util::generate_transaction_from_edits(
287 let transaction = util::generate_transaction_from_edits(
300 doc.text(),
288 doc.text(),
301 additional_edits,
289 additional_edits,
302− offset_encoding, // TODO: should probably transcode in Client
290+ item.offset_encoding, // TODO: should probably transcode in Client
303 );
291 );
304 doc.apply(&transaction, view.id);
292 doc.apply(&transaction, view.id);
305 }
293 }
@@ -323,10 +311,17 @@ impl Completion {
323 }
311 }
324
312
325 fn resolve_completion_item(
313 fn resolve_completion_item(
326− doc: &Document,
314+ language_server: &helix_lsp::Client,
327 completion_item: lsp::CompletionItem,
315 completion_item: lsp::CompletionItem,
328 ) -> Option<lsp::CompletionItem> {
316 ) -> 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+ }
330
325
331 let future = language_server.resolve_completion_item(completion_item)?;
326 let future = language_server.resolve_completion_item(completion_item)?;
332 let response = helix_lsp::block_on(future);
327 let response = helix_lsp::block_on(future);
@@ -397,8 +392,11 @@ impl Completion {
397 Some(item) if !item.resolved => item.clone(),
392 Some(item) if !item.resolved => item.clone(),
398 _ => return false,
393 _ => return false,
399 };
394 };
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+ {
402 Some(language_server) => language_server,
400 Some(language_server) => language_server,
403 None => return false,
401 None => return false,
404 };
402 };
@@ -422,13 +420,14 @@ impl Completion {
422 .unwrap()
420 .unwrap()
423 .completion
421 .completion
424 {
422 {
425− completion.replace_item(
426− current_item,
427− CompletionItem {
423+ let resolved_item = CompletionItem {
428− item: resolved_item,
424+ item: resolved_item,
425+ language_server_id: current_item.language_server_id,
426+ offset_encoding: current_item.offset_encoding,
429− resolved: true,
427+ resolved: true,
430− },
428+ };
431− );
429+
430+ completion.replace_item(current_item, resolved_item);
432 }
431 }
433 },
432 },
434 );
433 );
helix-term/src/ui/editor.rs+5−12
@@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
33
33
34use tui::{buffer::Buffer as Surface, text::Span};
34use tui::{buffer::Buffer as Surface, text::Span};
35
35
36−use super::statusline;
36+use super::{completion::CompletionItem, statusline};
37use super::{document::LineDecoration, lsp::SignatureHelp};
37use super::{document::LineDecoration, lsp::SignatureHelp};
38
38
39pub struct EditorView {
39pub struct EditorView {
@@ -650,7 +650,7 @@ impl EditorView {
650 .primary()
650 .primary()
651 .cursor(doc.text().slice(..));
651 .cursor(doc.text().slice(..));
652
652
653− let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
653+ let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
654 diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
655 });
655 });
656
656
@@ -953,20 +953,13 @@ impl EditorView {
953 &mut self,
953 &mut self,
954 editor: &mut Editor,
954 editor: &mut Editor,
955 savepoint: Arc<SavePoint>,
955 savepoint: Arc<SavePoint>,
956− items: Vec<helix_lsp::lsp::CompletionItem>,
956+ items: Vec<CompletionItem>,
957− offset_encoding: helix_lsp::OffsetEncoding,
958 start_offset: usize,
957 start_offset: usize,
959 trigger_offset: usize,
958 trigger_offset: usize,
960 size: Rect,
959 size: Rect,
961 ) -> Option<Rect> {
960 ) -> Option<Rect> {
962− let mut completion = Completion::new(
961+ let mut completion =
963− editor,
962+ Completion::new(editor, savepoint, items, start_offset, trigger_offset);
964− savepoint,
965− items,
966− offset_encoding,
967− start_offset,
968− trigger_offset,
969− );
970
963
971 if completion.is_empty() {
964 if completion.is_empty() {
972 // skip if we got no completion results
965 // skip if we got no completion results
helix-term/src/ui/mod.rs+9−12
@@ -17,7 +17,7 @@ mod text;
17use crate::compositor::{Component, Compositor};
17use crate::compositor::{Component, Compositor};
18use crate::filter_picker_entry;
18use crate::filter_picker_entry;
19use crate::job::{self, Callback};
19use crate::job::{self, Callback};
20−pub use completion::Completion;
20+pub use completion::{Completion, CompletionItem};
21pub use editor::EditorView;
21pub use editor::EditorView;
22pub use markdown::Markdown;
22pub use markdown::Markdown;
23pub use menu::Menu;
23pub use menu::Menu;
@@ -238,6 +238,7 @@ pub mod completers {
238 use crate::ui::prompt::Completion;
238 use crate::ui::prompt::Completion;
239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
239 use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
240 use fuzzy_matcher::FuzzyMatcher;
240 use fuzzy_matcher::FuzzyMatcher;
241+ use helix_core::syntax::LanguageServerFeature;
241 use helix_view::document::SCRATCH_BUFFER_NAME;
242 use helix_view::document::SCRATCH_BUFFER_NAME;
242 use helix_view::theme;
243 use helix_view::theme;
243 use helix_view::{editor::Config, Editor};
244 use helix_view::{editor::Config, Editor};
@@ -393,17 +394,13 @@ pub mod completers {
393 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394 let matcher = Matcher::default();
395 let matcher = Matcher::default();
395
396
396− let (_, doc) = current_ref!(editor);
397+ let language_servers =
397−
398+ doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand);
398− let language_server = match doc.language_server() {
399+ let options = match language_servers
399− Some(language_server) => language_server,
400+ .into_iter()
400− None => {
401− return vec![];
402− }
403− };
404−
405− let options = match &language_server.capabilities().execute_command_provider {
401+ .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
406− Some(options) => options,
402+ {
403+ Some(id_options) => id_options,
407 None => {
404 None => {
408 return vec![];
405 return vec![];
409 }
406 }
helix-term/src/ui/statusline.rs+6−6
@@ -197,15 +197,16 @@ where
197 );
197 );
198}
198}
199
199
200+// TODO think about handling multiple language servers
200fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
201where
202where
202 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203 F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
203{
204{
205+ let language_servers = context.doc.language_servers();
204 write(
206 write(
205 context,
207 context,
206− context
208+ language_servers
207− .doc
209+ .first()
208− .language_server()
209 .and_then(|srv| {
210 .and_then(|srv| {
210 context
211 context
211 .spinners
212 .spinners
@@ -225,8 +226,7 @@ where
225{
226{
226 let (warnings, errors) = context
227 let (warnings, errors) = context
227 .doc
228 .doc
228− .diagnostics()
229+ .shown_diagnostics()
229− .iter()
230 .fold((0, 0), |mut counts, diag| {
230 .fold((0, 0), |mut counts, diag| {
231 use helix_core::diagnostic::Severity;
231 use helix_core::diagnostic::Severity;
232 match diag.severity {
232 match diag.severity {
@@ -266,7 +266,7 @@ where
266 .diagnostics
266 .diagnostics
267 .values()
267 .values()
268 .flatten()
268 .flatten()
269− .fold((0, 0), |mut counts, diag| {
269+ .fold((0, 0), |mut counts, (diag, _, _)| {
270 match diag.severity {
270 match diag.severity {
271 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
271 Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
272 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
272 Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
helix-view/src/document.rs+104−41
@@ -6,7 +6,7 @@ use futures_util::FutureExt;
6use helix_core::auto_pairs::AutoPairs;
6use helix_core::auto_pairs::AutoPairs;
7use helix_core::doc_formatter::TextFormat;
7use helix_core::doc_formatter::TextFormat;
8use helix_core::encoding::Encoding;
8use helix_core::encoding::Encoding;
9−use helix_core::syntax::Highlight;
9+use helix_core::syntax::{Highlight, LanguageServerFeature, LanguageServerFeatureConfiguration};
10use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
10use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
11use helix_core::Range;
11use helix_core::Range;
12use helix_vcs::{DiffHandle, DiffProviderRegistry};
12use helix_vcs::{DiffHandle, DiffProviderRegistry};
@@ -16,7 +16,7 @@ use serde::de::{self, Deserialize, Deserializer};
16use serde::Serialize;
16use serde::Serialize;
17use std::borrow::Cow;
17use std::borrow::Cow;
18use std::cell::Cell;
18use std::cell::Cell;
19−use std::collections::HashMap;
19+use std::collections::{HashMap, HashSet};
20use std::fmt::Display;
20use std::fmt::Display;
21use std::future::Future;
21use std::future::Future;
22use std::path::{Path, PathBuf};
22use std::path::{Path, PathBuf};
@@ -180,7 +180,7 @@ pub struct Document {
180 pub(crate) modified_since_accessed: bool,
180 pub(crate) modified_since_accessed: bool,
181
181
182 diagnostics: Vec<Diagnostic>,
182 diagnostics: Vec<Diagnostic>,
183− language_server: Option<Arc<helix_lsp::Client>>,
183+ language_servers: Vec<Arc<helix_lsp::Client>>,
184
184
185 diff_handle: Option<DiffHandle>,
185 diff_handle: Option<DiffHandle>,
186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
186 version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@@ -616,7 +616,7 @@ impl Document {
616 last_saved_time: SystemTime::now(),
616 last_saved_time: SystemTime::now(),
617 last_saved_revision: 0,
617 last_saved_revision: 0,
618 modified_since_accessed: false,
618 modified_since_accessed: false,
619− language_server: None,
619+ language_servers: Vec::new(),
620 diff_handle: None,
620 diff_handle: None,
621 config,
621 config,
622 version_control_head: None,
622 version_control_head: None,
@@ -730,19 +730,24 @@ impl Document {
730 return Some(formatting_future.boxed());
730 return Some(formatting_future.boxed());
731 };
731 };
732
732
733− let language_server = self.language_server()?;
734 let text = self.text.clone();
733 let text = self.text.clone();
734+ // finds first language server that supports formatting and then formats
735+ let (offset_encoding, request) = self
736+ .language_servers_with_feature(LanguageServerFeature::Format)
737+ .iter()
738+ .find_map(|language_server| {
735− let offset_encoding = language_server.offset_encoding();
739+ let offset_encoding = language_server.offset_encoding();
736−
737− let request = language_server.text_document_formatting(
740+ let request = language_server.text_document_formatting(
738− self.identifier(),
741+ self.identifier(),
739− lsp::FormattingOptions {
742+ lsp::FormattingOptions {
740− tab_size: self.tab_width() as u32,
743+ tab_size: self.tab_width() as u32,
741− insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
744+ insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
742− ..Default::default()
745+ ..Default::default()
743− },
746+ },
744− None,
747+ None,
748+ )?;
749+ Some((offset_encoding, request))
745− )?;
750+ })?;
746
751
747 let fut = async move {
752 let fut = async move {
748 let edits = request.await.unwrap_or_else(|e| {
753 let edits = request.await.unwrap_or_else(|e| {
@@ -797,13 +802,12 @@ impl Document {
797 if self.path.is_none() {
802 if self.path.is_none() {
798 bail!("Can't save with no path set!");
803 bail!("Can't save with no path set!");
799 }
804 }
800−
801 self.path.as_ref().unwrap().clone()
805 self.path.as_ref().unwrap().clone()
802 }
806 }
803 };
807 };
804
808
805 let identifier = self.path().map(|_| self.identifier());
809 let identifier = self.path().map(|_| self.identifier());
806− let language_server = self.language_server.clone();
810+ let language_servers = self.language_servers.clone();
807
811
808 // mark changes up to now as saved
812 // mark changes up to now as saved
809 let current_rev = self.get_current_revision();
813 let current_rev = self.get_current_revision();
@@ -847,14 +851,13 @@ impl Document {
847 text: text.clone(),
851 text: text.clone(),
848 };
852 };
849
853
850− if let Some(language_server) = language_server {
854+ for language_server in language_servers {
851 if !language_server.is_initialized() {
855 if !language_server.is_initialized() {
852 return Ok(event);
856 return Ok(event);
853 }
857 }
854−
855− if let Some(identifier) = identifier {
858+ if let Some(identifier) = &identifier {
856 if let Some(notification) =
859 if let Some(notification) =
857− language_server.text_document_did_save(identifier, &text)
860+ language_server.text_document_did_save(identifier.clone(), &text)
858 {
861 {
859 notification.await?;
862 notification.await?;
860 }
863 }
@@ -1005,8 +1008,8 @@ impl Document {
1005 }
1008 }
1006
1009
1007 /// Set the LSP.
1010 /// Set the LSP.
1008− pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
1011+ pub fn set_language_servers(&mut self, language_servers: Vec<Arc<helix_lsp::Client>>) {
1009− self.language_server = language_server;
1012+ self.language_servers = language_servers;
1010 }
1013 }
1011
1014
1012 /// Select text within the [`Document`].
1015 /// Select text within the [`Document`].
@@ -1159,7 +1162,7 @@ impl Document {
1159
1162
1160 if emit_lsp_notification {
1163 if emit_lsp_notification {
1161 // emit lsp notification
1164 // emit lsp notification
1162− if let Some(language_server) = self.language_server() {
1165+ for language_server in self.language_servers() {
1163 let notify = language_server.text_document_did_change(
1166 let notify = language_server.text_document_did_change(
1164 self.versioned_identifier(),
1167 self.versioned_identifier(),
1165 &old_doc,
1168 &old_doc,
@@ -1415,18 +1418,13 @@ impl Document {
1415 .map(|language| language.language_id.as_str())
1418 .map(|language| language.language_id.as_str())
1416 }
1419 }
1417
1420
1418− /// Language ID for the document. Either the `language-id` from the
1421+ /// Language ID for the document. Either the `language-id`,
1419− /// `language-server` configuration, or the document language if no
1420− /// `language-id` has been specified.
1422+ /// or the document language name if no `language-id` has been specified.
1421 pub fn language_id(&self) -> Option<&str> {
1423 pub fn language_id(&self) -> Option<&str> {
1422− let language_config = self.language.as_deref()?;
1423−
1424− language_config
1424+ self.language_config()?
1425− .language_server
1425+ .language_server_language_id
1426− .as_ref()?
1427− .language_id
1428 .as_deref()
1426 .as_deref()
1429− .or(Some(language_config.language_id.as_str()))
1427+ .or_else(|| self.language_name())
1430 }
1428 }
1431
1429
1432 /// Corresponding [`LanguageConfiguration`].
1430 /// Corresponding [`LanguageConfiguration`].
@@ -1439,10 +1437,54 @@ impl Document {
1439 self.version
1437 self.version
1440 }
1438 }
1441
1439
1442− /// Language server if it has been initialized.
1440+ /// Language servers that have been initialized.
1443− pub fn language_server(&self) -> Option<&helix_lsp::Client> {
1441+ pub fn language_servers(&self) -> Vec<&helix_lsp::Client> {
1444− let server = self.language_server.as_deref()?;
1442+ self.language_servers
1445− server.is_initialized().then_some(server)
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()
1446 }
1488 }
1447
1489
1448 pub fn diff_handle(&self) -> Option<&DiffHandle> {
1490 pub fn diff_handle(&self) -> Option<&DiffHandle> {
@@ -1565,12 +1607,33 @@ impl Document {
1565 &self.diagnostics
1607 &self.diagnostics
1566 }
1608 }
1567
1609
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,
1568− pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
1623+ mut diagnostics: Vec<Diagnostic>,
1624+ language_server_id: usize,
1625+ ) {
1626+ self.clear_diagnostics(language_server_id);
1569− self.diagnostics = diagnostics;
1627+ self.diagnostics.append(&mut diagnostics);
1570 self.diagnostics
1628 self.diagnostics
1571 .sort_unstable_by_key(|diagnostic| diagnostic.range);
1629 .sort_unstable_by_key(|diagnostic| diagnostic.range);
1572 }
1630 }
1573
1631
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+
1574 /// Get the document's auto pairs. If the document has a recognized
1637 /// Get the document's auto pairs. If the document has a recognized
1575 /// language config with auto pairs configured, returns that;
1638 /// language config with auto pairs configured, returns that;
1576 /// otherwise, falls back to the global auto pairs config. If the global
1639 /// 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::{
48};
48};
49use helix_core::{Position, Selection};
49use helix_core::{Position, Selection};
50use helix_dap as dap;
50use helix_dap as dap;
51−use helix_lsp::lsp;
51+use helix_lsp::{lsp, OffsetEncoding};
52
52
53use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
53use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
54
54
@@ -689,7 +689,7 @@ pub struct WhitespaceCharacters {
689impl Default for WhitespaceCharacters {
689impl Default for WhitespaceCharacters {
690 fn default() -> Self {
690 fn default() -> Self {
691 Self {
691 Self {
692− space: '·', // U+00B7
692+ space: '·', // U+00B7
693 nbsp: '⍽', // U+237D
693 nbsp: '⍽', // U+237D
694 tab: '→', // U+2192
694 tab: '→', // U+2192
695 newline: '⏎', // U+23CE
695 newline: '⏎', // U+23CE
@@ -818,7 +818,7 @@ pub struct Editor {
818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
818 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
819 pub macro_replaying: Vec<char>,
819 pub macro_replaying: Vec<char>,
820 pub language_servers: helix_lsp::Registry,
820 pub language_servers: helix_lsp::Registry,
821− pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
821+ pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>,
822 pub diff_providers: DiffProviderRegistry,
822 pub diff_providers: DiffProviderRegistry,
823
823
824 pub debugger: Option<dap::Client>,
824 pub debugger: Option<dap::Client>,
@@ -941,6 +941,7 @@ impl Editor {
941 syn_loader: Arc<syntax::Loader>,
941 syn_loader: Arc<syntax::Loader>,
942 config: Arc<dyn DynAccess<Config>>,
942 config: Arc<dyn DynAccess<Config>>,
943 ) -> Self {
943 ) -> Self {
944+ let language_servers = helix_lsp::Registry::new(syn_loader.clone());
944 let conf = config.load();
945 let conf = config.load();
945 let auto_pairs = (&conf.auto_pairs).into();
946 let auto_pairs = (&conf.auto_pairs).into();
946
947
@@ -960,7 +961,7 @@ impl Editor {
960 macro_recording: None,
961 macro_recording: None,
961 macro_replaying: Vec::new(),
962 macro_replaying: Vec::new(),
962 theme: theme_loader.default(),
963 theme: theme_loader.default(),
963− language_servers: helix_lsp::Registry::new(),
964+ language_servers,
964 diagnostics: BTreeMap::new(),
965 diagnostics: BTreeMap::new(),
965 diff_providers: DiffProviderRegistry::default(),
966 diff_providers: DiffProviderRegistry::default(),
966 debugger: None,
967 debugger: None,
@@ -1093,12 +1094,12 @@ impl Editor {
1093 }
1094 }
1094
1095
1095 /// Refreshes the language server for a given document
1096 /// Refreshes the language server for a given document
1096− pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1097+ pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
1097− self.launch_language_server(doc_id)
1098+ self.launch_language_servers(doc_id)
1098 }
1099 }
1099
1100
1100 /// Launch a language server for a given document
1101 /// 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<()> {
1102 if !self.config().lsp.enable {
1103 if !self.config().lsp.enable {
1103 return None;
1104 return None;
1104 }
1105 }
@@ -1109,42 +1110,49 @@ impl Editor {
1109 let config = doc.config.load();
1110 let config = doc.config.load();
1110 let root_dirs = &config.workspace_lsp_roots;
1111 let root_dirs = &config.workspace_lsp_roots;
1111
1112
1112− // try to find a language server based on the language name
1113+ // try to find language servers based on the language name
1113− let language_server = lang.as_ref().and_then(|language| {
1114+ let language_servers = lang.as_ref().and_then(|language| {
1114 self.language_servers
1115 self.language_servers
1115 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1116 .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
1116 .map_err(|e| {
1117 .map_err(|e| {
1117 log::error!(
1118 log::error!(
1118− "Failed to initialize the LSP for `{}` {{ {} }}",
1119+ "Failed to initialize the language servers for `{}` {{ {} }}",
1119 language.scope(),
1120 language.scope(),
1120 e
1121 e
1121 )
1122 )
1122 })
1123 })
1123 .ok()
1124 .ok()
1124− .flatten()
1125 });
1125 });
1126
1126
1127 let doc = self.document_mut(doc_id)?;
1127 let doc = self.document_mut(doc_id)?;
1128 let doc_url = doc.url()?;
1128 let doc_url = doc.url()?;
1129
1129
1130− if let Some(language_server) = language_server {
1130+ if let Some(language_servers) = language_servers {
1131− // only spawn a new lang server if the servers aren't the same
1131+ // only spawn new lang servers if the servers aren't the same
1132− if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
1132+ let doc_language_servers = doc.language_servers();
1133− if let Some(language_server) = doc.language_server() {
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 {
1134− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1140+ tokio::spawn(doc_language_server.text_document_did_close(doc.identifier()));
1135 }
1141 }
1136
1142
1137 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1143 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1138
1144
1145+ for language_server in &language_servers {
1139− // TODO: this now races with on_init code if the init happens too quickly
1146+ // TODO: this now races with on_init code if the init happens too quickly
1140− tokio::spawn(language_server.text_document_did_open(
1147+ tokio::spawn(language_server.text_document_did_open(
1141− doc_url,
1148+ doc_url.clone(),
1142− doc.version(),
1149+ doc.version(),
1143− doc.text(),
1150+ doc.text(),
1144− language_id,
1151+ language_id.clone(),
1145− ));
1152+ ));
1153+ }
1146
1154
1147− doc.set_language_server(Some(language_server));
1155+ doc.set_language_servers(language_servers);
1148 }
1156 }
1149 }
1157 }
1150 Some(())
1158 Some(())
@@ -1337,10 +1345,10 @@ impl Editor {
1337 }
1345 }
1338 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1346 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1339
1347
1340− let id = self.new_document(doc);
1348+ let doc_id = self.new_document(doc);
1341− let _ = self.launch_language_server(id);
1349+ let _ = self.launch_language_servers(doc_id);
1342
1350
1343− id
1351+ doc_id
1344 };
1352 };
1345
1353
1346 self.switch(id, action);
1354 self.switch(id, action);
@@ -1368,7 +1376,7 @@ impl Editor {
1368 // This will also disallow any follow-up writes
1376 // This will also disallow any follow-up writes
1369 self.saves.remove(&doc_id);
1377 self.saves.remove(&doc_id);
1370
1378
1371− if let Some(language_server) = doc.language_server() {
1379+ for language_server in doc.language_servers() {
1372 // TODO: track error
1380 // TODO: track error
1373 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1381 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1374 }
1382 }
helix-view/src/gutter.rs+1−1
@@ -55,7 +55,7 @@ pub fn diagnostic<'doc>(
55 let error = theme.get("error");
55 let error = theme.get("error");
56 let info = theme.get("info");
56 let info = theme.get("info");
57 let hint = theme.get("hint");
57 let hint = theme.get("hint");
58− let diagnostics = doc.diagnostics();
58+ let diagnostics = doc.shown_diagnostics().collect::<Vec<_>>();
59
59
60 Box::new(
60 Box::new(
61 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
61 move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
languages.toml+245−180
@@ -1,6 +1,153 @@
1# Language support configuration.
1# Language support configuration.
2# See the languages documentation: https://docs.helix-editor.com/master/languages.html
2# See the languages documentation: https://docs.helix-editor.com/master/languages.html
3
3
4+[language-server]
5+
6+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+
4[[language]]
151[[language]]
5name = "rust"
152name = "rust"
6scope = "source.rust"
153scope = "source.rust"
@@ -9,7 +156,7 @@ file-types = ["rs"]
9roots = ["Cargo.toml", "Cargo.lock"]
156roots = ["Cargo.toml", "Cargo.lock"]
10auto-format = true
157auto-format = true
11comment-token = "//"
158comment-token = "//"
12−language-server = { command = "rust-analyzer" }
159+language-servers = [ "rust-analyzer" ]
13indent = { tab-width = 4, unit = " " }
160indent = { tab-width = 4, unit = " " }
14
161
15[language.auto-pairs]
162[language.auto-pairs]
@@ -19,14 +166,6 @@ indent = { tab-width = 4, unit = " " }
19'"' = '"'
166'"' = '"'
20'`' = '`'
167'`' = '`'
21
168
22−[language.config]
23−inlayHints.bindingModeHints.enable = false
24−inlayHints.closingBraceHints.minLines = 10
25−inlayHints.closureReturnTypeHints.enable = "with_block"
26−inlayHints.discriminantHints.enable = "fieldless"
27−inlayHints.lifetimeElisionHints.enable = "skip_trivial"
28−inlayHints.typeHints.hideClosureInitialization = false
29−
30[language.debugger]
169[language.debugger]
31name = "lldb-vscode"
170name = "lldb-vscode"
32transport = "stdio"
171transport = "stdio"
@@ -65,7 +204,7 @@ name = "sway"
65scope = "source.sway"
204scope = "source.sway"
66injection-regex = "sway"
205injection-regex = "sway"
67file-types = ["sw"]
206file-types = ["sw"]
68−language-server = { command = "forc", args = ["lsp"] }
207+language-servers = [ "forc" ]
69roots = ["Forc.toml", "Forc.lock"]
208roots = ["Forc.toml", "Forc.lock"]
70indent = { tab-width = 4, unit = " " }
209indent = { tab-width = 4, unit = " " }
71comment-token = "//"
210comment-token = "//"
@@ -81,7 +220,7 @@ injection-regex = "toml"
81file-types = ["toml", "poetry.lock"]
220file-types = ["toml", "poetry.lock"]
82roots = []
221roots = []
83comment-token = "#"
222comment-token = "#"
84−language-server = { command = "taplo", args = ["lsp", "stdio"] }
223+language-servers = [ "taplo" ]
85indent = { tab-width = 2, unit = " " }
224indent = { tab-width = 2, unit = " " }
86
225
87[[grammar]]
226[[grammar]]
@@ -95,7 +234,7 @@ injection-regex = "awk"
95file-types = ["awk", "gawk", "nawk", "mawk"]
234file-types = ["awk", "gawk", "nawk", "mawk"]
96roots = []
235roots = []
97comment-token = "#"
236comment-token = "#"
98−language-server = { command = "awk-language-server" }
237+language-servers = [ "awk-language-server" ]
99indent = { tab-width = 2, unit = " " }
238indent = { tab-width = 2, unit = " " }
100
239
101[[grammar]]
240[[grammar]]
@@ -123,8 +262,7 @@ file-types = ["ex", "exs", "mix.lock"]
123shebangs = ["elixir"]
262shebangs = ["elixir"]
124roots = ["mix.exs", "mix.lock"]
263roots = ["mix.exs", "mix.lock"]
125comment-token = "#"
264comment-token = "#"
126−language-server = { command = "elixir-ls" }
265+language-servers = [ "elixir-ls" ]
127−config = { elixirLS.dialyzerEnabled = false }
128indent = { tab-width = 2, unit = " " }
266indent = { tab-width = 2, unit = " " }
129
267
130[[grammar]]
268[[grammar]]
@@ -153,7 +291,7 @@ file-types = ["mint"]
153shebangs = []
291shebangs = []
154roots = []
292roots = []
155comment-token = "//"
293comment-token = "//"
156−language-server = { command = "mint", args = ["ls"] }
294+language-servers = [ "mint" ]
157indent = { tab-width = 2, unit = " " }
295indent = { tab-width = 2, unit = " " }
158
296
159[[language]]
297[[language]]
@@ -162,9 +300,8 @@ scope = "source.json"
162injection-regex = "json"
300injection-regex = "json"
163file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
301file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
164roots = []
302roots = []
165−language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
303+language-servers = [ "vscode-json-language-server" ]
166auto-format = true
304auto-format = true
167−config = { "provideFormatter" = true }
168indent = { tab-width = 2, unit = " " }
305indent = { tab-width = 2, unit = " " }
169
306
170[[grammar]]
307[[grammar]]
@@ -178,7 +315,7 @@ injection-regex = "c"
178file-types = ["c"] # TODO: ["h"]
315file-types = ["c"] # TODO: ["h"]
179roots = []
316roots = []
180comment-token = "//"
317comment-token = "//"
181−language-server = { command = "clangd" }
318+language-servers = [ "clangd" ]
182indent = { tab-width = 2, unit = " " }
319indent = { tab-width = 2, unit = " " }
183
320
184[language.debugger]
321[language.debugger]
@@ -215,7 +352,7 @@ injection-regex = "cpp"
215file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
352file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
216roots = []
353roots = []
217comment-token = "//"
354comment-token = "//"
218−language-server = { command = "clangd" }
355+language-servers = [ "clangd" ]
219indent = { tab-width = 2, unit = " " }
356indent = { tab-width = 2, unit = " " }
220
357
221[language.debugger]
358[language.debugger]
@@ -253,7 +390,7 @@ roots = ["shard.yml", "shard.lock"]
253comment-token = "#"
390comment-token = "#"
254indent = { tab-width = 2, unit = " " }
391indent = { tab-width = 2, unit = " " }
255grammar = "ruby"
392grammar = "ruby"
256−language-server = { command = "crystalline", args = ["--stdio"] }
393+language-servers = [ "crystalline" ]
257
394
258[[language]]
395[[language]]
259name = "c-sharp"
396name = "c-sharp"
@@ -263,7 +400,7 @@ file-types = ["cs"]
263roots = ["sln", "csproj"]
400roots = ["sln", "csproj"]
264comment-token = "//"
401comment-token = "//"
265indent = { tab-width = 4, unit = "\t" }
402indent = { tab-width = 4, unit = "\t" }
266−language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
403+language-servers = [ "omnisharp" ]
267
404
268[language.debugger]
405[language.debugger]
269name = "netcoredbg"
406name = "netcoredbg"
@@ -296,18 +433,10 @@ file-types = ["go"]
296roots = ["go.work", "go.mod"]
433roots = ["go.work", "go.mod"]
297auto-format = true
434auto-format = true
298comment-token = "//"
435comment-token = "//"
299−language-server = { command = "gopls" }
436+language-servers = [ "gopls" ]
300# TODO: gopls needs utf-8 offsets?
437# TODO: gopls needs utf-8 offsets?
301indent = { tab-width = 4, unit = "\t" }
438indent = { tab-width = 4, unit = "\t" }
302
439
303−[language.config.hints]
304−assignVariableTypes = true
305−compositeLiteralFields = true
306−constantValues = true
307−functionTypeParameters = true
308−parameterNames = true
309−rangeVariableTypes = true
310−
311[language.debugger]
440[language.debugger]
312name = "go"
441name = "go"
313transport = "tcp"
442transport = "tcp"
@@ -351,7 +480,7 @@ file-types = ["go.mod"]
351roots = []
480roots = []
352auto-format = true
481auto-format = true
353comment-token = "//"
482comment-token = "//"
354−language-server = { command = "gopls" }
483+language-servers = [ "gopls" ]
355indent = { tab-width = 4, unit = "\t" }
484indent = { tab-width = 4, unit = "\t" }
356
485
357[[grammar]]
486[[grammar]]
@@ -365,7 +494,7 @@ injection-regex = "gotmpl"
365file-types = ["gotmpl"]
494file-types = ["gotmpl"]
366roots = []
495roots = []
367comment-token = "//"
496comment-token = "//"
368−language-server = { command = "gopls" }
497+language-servers = [ "gopls" ]
369indent = { tab-width = 2, unit = " " }
498indent = { tab-width = 2, unit = " " }
370
499
371[[grammar]]
500[[grammar]]
@@ -380,7 +509,7 @@ file-types = ["go.work"]
380roots = []
509roots = []
381auto-format = true
510auto-format = true
382comment-token = "//"
511comment-token = "//"
383−language-server = { command = "gopls" }
512+language-servers = [ "gopls" ]
384indent = { tab-width = 4, unit = "\t" }
513indent = { tab-width = 4, unit = "\t" }
385
514
386[[grammar]]
515[[grammar]]
@@ -391,26 +520,15 @@ source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd
391name = "javascript"
520name = "javascript"
392scope = "source.js"
521scope = "source.js"
393injection-regex = "(js|javascript)"
522injection-regex = "(js|javascript)"
523+language-id = "javascript"
394file-types = ["js", "mjs", "cjs"]
524file-types = ["js", "mjs", "cjs"]
395shebangs = ["node"]
525shebangs = ["node"]
396roots = []
526roots = []
397comment-token = "//"
527comment-token = "//"
398# TODO: highlights-params
528# TODO: highlights-params
399−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
529+language-servers = [ "typescript-language-server" ]
400indent = { tab-width = 2, unit = " " }
530indent = { tab-width = 2, unit = " " }
401
531
402−[language.config]
403−hostInfo = "helix"
404−
405−[language.config.javascript.inlayHints]
406−includeInlayEnumMemberValueHints = true
407−includeInlayFunctionLikeReturnTypeHints = true
408−includeInlayFunctionParameterTypeHints = true
409−includeInlayParameterNameHints = "all"
410−includeInlayParameterNameHintsWhenArgumentMatchesName = true
411−includeInlayPropertyDeclarationTypeHints = true
412−includeInlayVariableTypeHints = true
413−
414[language.debugger]
532[language.debugger]
415name = "node-debug2"
533name = "node-debug2"
416transport = "stdio"
534transport = "stdio"
@@ -431,48 +549,26 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev =
431name = "jsx"
549name = "jsx"
432scope = "source.jsx"
550scope = "source.jsx"
433injection-regex = "jsx"
551injection-regex = "jsx"
552+language-id = "javascriptreact"
434file-types = ["jsx"]
553file-types = ["jsx"]
435roots = []
554roots = []
436comment-token = "//"
555comment-token = "//"
437−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascriptreact" }
556+language-servers = [ "typescript-language-server" ]
438indent = { tab-width = 2, unit = " " }
557indent = { tab-width = 2, unit = " " }
439grammar = "javascript"
558grammar = "javascript"
440
559
441−[language.config]
442−hostInfo = "helix"
443−
444−[language.config.javascript.inlayHints]
445−includeInlayEnumMemberValueHints = true
446−includeInlayFunctionLikeReturnTypeHints = true
447−includeInlayFunctionParameterTypeHints = true
448−includeInlayParameterNameHints = "all"
449−includeInlayParameterNameHintsWhenArgumentMatchesName = true
450−includeInlayPropertyDeclarationTypeHints = true
451−includeInlayVariableTypeHints = true
452−
453[[language]]
560[[language]]
454name = "typescript"
561name = "typescript"
455scope = "source.ts"
562scope = "source.ts"
456injection-regex = "(ts|typescript)"
563injection-regex = "(ts|typescript)"
457file-types = ["ts", "mts", "cts"]
564file-types = ["ts", "mts", "cts"]
565+language-id = "typescript"
458shebangs = []
566shebangs = []
459roots = []
567roots = []
460# TODO: highlights-params
568# TODO: highlights-params
461−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
569+language-servers = [ "typescript-language-server" ]
462indent = { tab-width = 2, unit = " " }
570indent = { tab-width = 2, unit = " " }
463
571
464−[language.config]
465−hostInfo = "helix"
466−
467−[language.config.typescript.inlayHints]
468−includeInlayEnumMemberValueHints = true
469−includeInlayFunctionLikeReturnTypeHints = true
470−includeInlayFunctionParameterTypeHints = true
471−includeInlayParameterNameHints = "all"
472−includeInlayParameterNameHintsWhenArgumentMatchesName = true
473−includeInlayPropertyDeclarationTypeHints = true
474−includeInlayVariableTypeHints = true
475−
476[[grammar]]
572[[grammar]]
477name = "typescript"
573name = "typescript"
478source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
574source = { 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 =
481name = "tsx"
577name = "tsx"
482scope = "source.tsx"
578scope = "source.tsx"
483injection-regex = "(tsx)" # |typescript
579injection-regex = "(tsx)" # |typescript
580+language-id = "typescriptreact"
484file-types = ["tsx"]
581file-types = ["tsx"]
485roots = []
582roots = []
486# TODO: highlights-params
583# TODO: highlights-params
487−language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
584+language-servers = [ "typescript-language-server" ]
488indent = { tab-width = 2, unit = " " }
585indent = { tab-width = 2, unit = " " }
489
586
490−[language.config]
491−hostInfo = "helix"
492−
493−[language.config.typescript.inlayHints]
494−includeInlayEnumMemberValueHints = true
495−includeInlayFunctionLikeReturnTypeHints = true
496−includeInlayFunctionParameterTypeHints = true
497−includeInlayParameterNameHints = "all"
498−includeInlayParameterNameHintsWhenArgumentMatchesName = true
499−includeInlayPropertyDeclarationTypeHints = true
500−includeInlayVariableTypeHints = true
501−
502[[grammar]]
587[[grammar]]
503name = "tsx"
588name = "tsx"
504source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
589source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@@ -509,9 +594,8 @@ scope = "source.css"
509injection-regex = "css"
594injection-regex = "css"
510file-types = ["css", "scss"]
595file-types = ["css", "scss"]
511roots = []
596roots = []
512−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
597+language-servers = [ "vscode-css-language-server" ]
513auto-format = true
598auto-format = true
514−config = { "provideFormatter" = true }
515indent = { tab-width = 2, unit = " " }
599indent = { tab-width = 2, unit = " " }
516
600
517[[grammar]]
601[[grammar]]
@@ -524,9 +608,8 @@ scope = "source.scss"
524injection-regex = "scss"
608injection-regex = "scss"
525file-types = ["scss"]
609file-types = ["scss"]
526roots = []
610roots = []
527−language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
611+language-servers = [ "vscode-css-language-server" ]
528auto-format = true
612auto-format = true
529−config = { "provideFormatter" = true }
530indent = { tab-width = 2, unit = " " }
613indent = { tab-width = 2, unit = " " }
531
614
532[[grammar]]
615[[grammar]]
@@ -539,9 +622,8 @@ scope = "text.html.basic"
539injection-regex = "html"
622injection-regex = "html"
540file-types = ["html"]
623file-types = ["html"]
541roots = []
624roots = []
542−language-server = { command = "vscode-html-language-server", args = ["--stdio"] }
625+language-servers = [ "vscode-html-language-server" ]
543auto-format = true
626auto-format = true
544−config = { "provideFormatter" = true }
545indent = { tab-width = 2, unit = " " }
627indent = { tab-width = 2, unit = " " }
546
628
547[[grammar]]
629[[grammar]]
@@ -556,7 +638,7 @@ file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstr
556shebangs = ["python"]
638shebangs = ["python"]
557roots = []
639roots = []
558comment-token = "#"
640comment-token = "#"
559−language-server = { command = "pylsp" }
641+language-servers = [ "pylsp" ]
560# TODO: pyls needs utf-8 offsets
642# TODO: pyls needs utf-8 offsets
561indent = { tab-width = 4, unit = " " }
643indent = { tab-width = 4, unit = " " }
562
644
@@ -572,7 +654,7 @@ file-types = ["ncl"]
572shebangs = []
654shebangs = []
573roots = []
655roots = []
574comment-token = "#"
656comment-token = "#"
575−language-server = { command = "nls" }
657+language-servers = [ "nls" ]
576indent = { tab-width = 2, unit = " " }
658indent = { tab-width = 2, unit = " " }
577
659
578[[grammar]]
660[[grammar]]
@@ -587,7 +669,7 @@ file-types = ["nix"]
587shebangs = []
669shebangs = []
588roots = []
670roots = []
589comment-token = "#"
671comment-token = "#"
590−language-server = { command = "nil" }
672+language-servers = [ "nil" ]
591indent = { tab-width = 2, unit = " " }
673indent = { tab-width = 2, unit = " " }
592
674
593[[grammar]]
675[[grammar]]
@@ -602,7 +684,7 @@ file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile",
602shebangs = ["ruby"]
684shebangs = ["ruby"]
603roots = []
685roots = []
604comment-token = "#"
686comment-token = "#"
605−language-server = { command = "solargraph", args = ["stdio"] }
687+language-servers = [ "solargraph" ]
606indent = { tab-width = 2, unit = " " }
688indent = { tab-width = 2, unit = " " }
607
689
608[[grammar]]
690[[grammar]]
@@ -617,7 +699,7 @@ file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile
617shebangs = ["sh", "bash", "dash", "zsh"]
699shebangs = ["sh", "bash", "dash", "zsh"]
618roots = []
700roots = []
619comment-token = "#"
701comment-token = "#"
620−language-server = { command = "bash-language-server", args = ["start"] }
702+language-servers = [ "bash-language-server" ]
621indent = { tab-width = 2, unit = " " }
703indent = { tab-width = 2, unit = " " }
622
704
623[[grammar]]
705[[grammar]]
@@ -631,7 +713,7 @@ injection-regex = "php"
631file-types = ["php", "inc"]
713file-types = ["php", "inc"]
632shebangs = ["php"]
714shebangs = ["php"]
633roots = ["composer.json", "index.php"]
715roots = ["composer.json", "index.php"]
634−language-server = { command = "intelephense", args = ["--stdio"] }
716+language-servers = [ "intelephense" ]
635indent = { tab-width = 4, unit = " " }
717indent = { tab-width = 4, unit = " " }
636
718
637[[grammar]]
719[[grammar]]
@@ -657,7 +739,7 @@ injection-regex = "tex"
657file-types = ["tex", "sty", "cls"]
739file-types = ["tex", "sty", "cls"]
658roots = []
740roots = []
659comment-token = "%"
741comment-token = "%"
660−language-server = { command = "texlab" }
742+language-servers = [ "texlab" ]
661indent = { tab-width = 4, unit = "\t" }
743indent = { tab-width = 4, unit = "\t" }
662
744
663[[grammar]]
745[[grammar]]
@@ -671,7 +753,7 @@ injection-regex = "bib"
671file-types = ["bib"]
753file-types = ["bib"]
672roots = []
754roots = []
673comment-token = "%"
755comment-token = "%"
674−language-server = { command = "texlab" }
756+language-servers = [ "texlab" ]
675indent = { tab-width = 4, unit = "\t" }
757indent = { tab-width = 4, unit = "\t" }
676auto-format = true
758auto-format = true
677
759
@@ -699,7 +781,7 @@ injection-regex = "lean"
699file-types = ["lean"]
781file-types = ["lean"]
700roots = [ "lakefile.lean" ]
782roots = [ "lakefile.lean" ]
701comment-token = "--"
783comment-token = "--"
702−language-server = { command = "lean", args = [ "--server" ] }
784+language-servers = [ "lean" ]
703indent = { tab-width = 2, unit = " " }
785indent = { tab-width = 2, unit = " " }
704
786
705[[grammar]]
787[[grammar]]
@@ -713,13 +795,7 @@ injection-regex = "julia"
713file-types = ["jl"]
795file-types = ["jl"]
714roots = ["Manifest.toml", "Project.toml"]
796roots = ["Manifest.toml", "Project.toml"]
715comment-token = "#"
797comment-token = "#"
716−language-server = { command = "julia", timeout = 60, args = [
798+language-servers = [ "julia" ]
717− "--startup-file=no",
718− "--history-file=no",
719− "--quiet",
720− "-e",
721− "using LanguageServer; runserver()",
722− ] }
723indent = { tab-width = 4, unit = " " }
799indent = { tab-width = 4, unit = " " }
724
800
725[[grammar]]
801[[grammar]]
@@ -732,7 +808,7 @@ scope = "source.java"
732injection-regex = "java"
808injection-regex = "java"
733file-types = ["java"]
809file-types = ["java"]
734roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
810roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
735−language-server = { command = "jdtls" }
811+language-servers = [ "jdtls" ]
736indent = { tab-width = 4, unit = " " }
812indent = { tab-width = 4, unit = " " }
737
813
738[[grammar]]
814[[grammar]]
@@ -773,7 +849,7 @@ file-types = ["ml"]
773shebangs = []
849shebangs = []
774roots = []
850roots = []
775comment-token = "(**)"
851comment-token = "(**)"
776−language-server = { command = "ocamllsp" }
852+language-servers = [ "ocamllsp" ]
777indent = { tab-width = 2, unit = " " }
853indent = { tab-width = 2, unit = " " }
778
854
779[language.auto-pairs]
855[language.auto-pairs]
@@ -794,7 +870,7 @@ file-types = ["mli"]
794shebangs = []
870shebangs = []
795roots = []
871roots = []
796comment-token = "(**)"
872comment-token = "(**)"
797−language-server = { command = "ocamllsp" }
873+language-servers = [ "ocamllsp" ]
798indent = { tab-width = 2, unit = " " }
874indent = { tab-width = 2, unit = " " }
799
875
800[language.auto-pairs]
876[language.auto-pairs]
@@ -817,15 +893,7 @@ shebangs = ["lua"]
817roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
893roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
818comment-token = "--"
894comment-token = "--"
819indent = { tab-width = 2, unit = " " }
895indent = { tab-width = 2, unit = " " }
820−language-server = { command = "lua-language-server", args = [] }
896+language-servers = [ "lua-language-server" ]
821−
822−[language.config.Lua.hint]
823−enable = true
824−arrayIndex = "Enable"
825−setType = true
826−paramName = "All"
827−paramType = true
828−await = true
829
897
830[[grammar]]
898[[grammar]]
831name = "lua"
899name = "lua"
@@ -838,7 +906,7 @@ injection-regex = "svelte"
838file-types = ["svelte"]
906file-types = ["svelte"]
839roots = []
907roots = []
840indent = { tab-width = 2, unit = " " }
908indent = { tab-width = 2, unit = " " }
841−language-server = { command = "svelteserver", args = ["--stdio"] }
909+language-servers = [ "svelteserver" ]
842
910
843[[grammar]]
911[[grammar]]
844name = "svelte"
912name = "svelte"
@@ -851,7 +919,7 @@ injection-regex = "vue"
851file-types = ["vue"]
919file-types = ["vue"]
852roots = ["package.json", "vue.config.js"]
920roots = ["package.json", "vue.config.js"]
853indent = { tab-width = 2, unit = " " }
921indent = { tab-width = 2, unit = " " }
854−language-server = { command = "vls" }
922+language-servers = [ "vuels" ]
855
923
856[[grammar]]
924[[grammar]]
857name = "vue"
925name = "vue"
@@ -864,7 +932,7 @@ file-types = ["yml", "yaml"]
864roots = []
932roots = []
865comment-token = "#"
933comment-token = "#"
866indent = { tab-width = 2, unit = " " }
934indent = { tab-width = 2, unit = " " }
867−language-server = { command = "yaml-language-server", args = ["--stdio"] }
935+language-servers = [ "yaml-language-server" ]
868injection-regex = "yml|yaml"
936injection-regex = "yml|yaml"
869
937
870[[grammar]]
938[[grammar]]
@@ -878,7 +946,7 @@ injection-regex = "haskell"
878file-types = ["hs", "hs-boot"]
946file-types = ["hs", "hs-boot"]
879roots = ["Setup.hs", "stack.yaml", "cabal.project"]
947roots = ["Setup.hs", "stack.yaml", "cabal.project"]
880comment-token = "--"
948comment-token = "--"
881−language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
949+language-servers = [ "haskell-language-server" ]
882indent = { tab-width = 2, unit = " " }
950indent = { tab-width = 2, unit = " " }
883
951
884[[grammar]]
952[[grammar]]
@@ -892,7 +960,7 @@ injection-regex = "purescript"
892file-types = ["purs"]
960file-types = ["purs"]
893roots = ["spago.dhall", "bower.json"]
961roots = ["spago.dhall", "bower.json"]
894comment-token = "--"
962comment-token = "--"
895−language-server = { command = "purescript-language-server", args = ["--stdio"] }
963+language-servers = [ "purescript-language-server" ]
896indent = { tab-width = 2, unit = " " }
964indent = { tab-width = 2, unit = " " }
897auto-format = true
965auto-format = true
898formatter = { command = "purs-tidy", args = ["format"] }
966formatter = { command = "purs-tidy", args = ["format"] }
@@ -906,7 +974,7 @@ file-types = ["zig"]
906roots = ["build.zig"]
974roots = ["build.zig"]
907auto-format = true
975auto-format = true
908comment-token = "//"
976comment-token = "//"
909−language-server = { command = "zls" }
977+language-servers = [ "zls" ]
910indent = { tab-width = 4, unit = " " }
978indent = { tab-width = 4, unit = " " }
911formatter = { command = "zig" , args = ["fmt", "--stdin"] }
979formatter = { command = "zig" , args = ["fmt", "--stdin"] }
912
980
@@ -944,10 +1012,7 @@ roots = []
944file-types = ["pl", "prolog"]
1012file-types = ["pl", "prolog"]
945shebangs = ["swipl"]
1013shebangs = ["swipl"]
946comment-token = "%"
1014comment-token = "%"
947−language-server = { command = "swipl", args = [
1015+language-servers = [ "swipl" ]
948− "-g", "use_module(library(lsp_server))",
949− "-g", "lsp_server:main",
950− "-t", "halt", "--", "stdio"] }
951
1016
952[[language]]
1017[[language]]
953name = "tsq"
1018name = "tsq"
@@ -969,7 +1034,7 @@ file-types = ["cmake", "CMakeLists.txt"]
969roots = []
1034roots = []
970comment-token = "#"
1035comment-token = "#"
971indent = { tab-width = 2, unit = " " }
1036indent = { tab-width = 2, unit = " " }
972−language-server = { command = "cmake-language-server" }
1037+language-servers = [ "cmake-language-server" ]
973injection-regex = "cmake"
1038injection-regex = "cmake"
974
1039
975[[grammar]]
1040[[grammar]]
@@ -1009,7 +1074,7 @@ file-types = ["pl", "pm", "t"]
1009shebangs = ["perl"]
1074shebangs = ["perl"]
1010roots = []
1075roots = []
1011comment-token = "#"
1076comment-token = "#"
1012−language-server = { command = "perlnavigator", args= ["--stdio"] }
1077+language-servers = [ "perlnavigator" ]
1013indent = { tab-width = 2, unit = " " }
1078indent = { tab-width = 2, unit = " " }
1014
1079
1015[[grammar]]
1080[[grammar]]
@@ -1023,7 +1088,7 @@ roots = []
1023file-types = ["rkt", "rktd", "rktl", "scrbl"]
1088file-types = ["rkt", "rktd", "rktl", "scrbl"]
1024shebangs = ["racket"]
1089shebangs = ["racket"]
1025comment-token = ";"
1090comment-token = ";"
1026−language-server = { command = "racket", args = ["-l", "racket-langserver"] }
1091+language-servers = [ "racket" ]
1027grammar = "scheme"
1092grammar = "scheme"
1028
1093
1029[[language]]
1094[[language]]
@@ -1034,7 +1099,7 @@ file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
1034shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1099shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
1035comment-token = ";"
1100comment-token = ";"
1036indent = { tab-width = 2, unit = " " }
1101indent = { tab-width = 2, unit = " " }
1037−language-server = { command = "cl-lsp", args = [ "stdio" ] }
1102+language-servers = [ "cl-lsp" ]
1038grammar = "scheme"
1103grammar = "scheme"
1039
1104
1040[language.auto-pairs]
1105[language.auto-pairs]
@@ -1060,7 +1125,7 @@ scope = "source.wgsl"
1060file-types = ["wgsl"]
1125file-types = ["wgsl"]
1061roots = []
1126roots = []
1062comment-token = "//"
1127comment-token = "//"
1063−language-server = { command = "wgsl_analyzer" }
1128+language-servers = [ "wgsl_analyzer" ]
1064indent = { tab-width = 4, unit = " " }
1129indent = { tab-width = 4, unit = " " }
1065
1130
1066[[grammar]]
1131[[grammar]]
@@ -1123,7 +1188,7 @@ scope = "source.md"
1123injection-regex = "md|markdown"
1188injection-regex = "md|markdown"
1124file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1189file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
1125roots = [".marksman.toml"]
1190roots = [".marksman.toml"]
1126−language-server = { command = "marksman", args=["server"] }
1191+language-servers = [ "marksman" ]
1127indent = { tab-width = 2, unit = " " }
1192indent = { tab-width = 2, unit = " " }
1128
1193
1129[[grammar]]
1194[[grammar]]
@@ -1149,7 +1214,7 @@ file-types = ["dart"]
1149roots = ["pubspec.yaml"]
1214roots = ["pubspec.yaml"]
1150auto-format = true
1215auto-format = true
1151comment-token = "//"
1216comment-token = "//"
1152−language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
1217+language-servers = [ "dart" ]
1153indent = { tab-width = 2, unit = " " }
1218indent = { tab-width = 2, unit = " " }
1154
1219
1155[[grammar]]
1220[[grammar]]
@@ -1163,8 +1228,7 @@ roots = ["build.sbt", "build.sc", "build.gradle", "build.gradle.kts", "pom.xml",
1163file-types = ["scala", "sbt", "sc"]
1228file-types = ["scala", "sbt", "sc"]
1164comment-token = "//"
1229comment-token = "//"
1165indent = { tab-width = 2, unit = " " }
1230indent = { tab-width = 2, unit = " " }
1166−language-server = { command = "metals" }
1231+language-servers = [ "metals" ]
1167−config = { "isHttpEnabled" = true }
1168
1232
1169[[grammar]]
1233[[grammar]]
1170name = "scala"
1234name = "scala"
@@ -1178,7 +1242,7 @@ roots = ["Dockerfile", "Containerfile"]
1178file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
1242file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
1179comment-token = "#"
1243comment-token = "#"
1180indent = { tab-width = 2, unit = " " }
1244indent = { tab-width = 2, unit = " " }
1181−language-server = { command = "docker-langserver", args = ["--stdio"] }
1245+language-servers = [ "docker-langserver" ]
1182
1246
1183[[grammar]]
1247[[grammar]]
1184name = "dockerfile"
1248name = "dockerfile"
@@ -1218,7 +1282,7 @@ roots = []
1218file-types = ["git-rebase-todo"]
1282file-types = ["git-rebase-todo"]
1219injection-regex = "git-rebase"
1283injection-regex = "git-rebase"
1220comment-token = "#"
1284comment-token = "#"
1221−indent = { tab-width = 2, unit = " " }
1285+indent = { tab-width = 2, unit = "y" }
1222
1286
1223[[grammar]]
1287[[grammar]]
1224name = "git-rebase"
1288name = "git-rebase"
@@ -1294,7 +1358,7 @@ file-types = ["elm"]
1294roots = ["elm.json"]
1358roots = ["elm.json"]
1295auto-format = true
1359auto-format = true
1296comment-token = "--"
1360comment-token = "--"
1297−language-server = { command = "elm-language-server" }
1361+language-servers = [ "elm-language-server" ]
1298indent = { tab-width = 4, unit = " " }
1362indent = { tab-width = 4, unit = " " }
1299
1363
1300[[grammar]]
1364[[grammar]]
@@ -1320,7 +1384,7 @@ file-types = ["res"]
1320roots = ["bsconfig.json"]
1384roots = ["bsconfig.json"]
1321auto-format = true
1385auto-format = true
1322comment-token = "//"
1386comment-token = "//"
1323−language-server = { command = "rescript-language-server", args = ["--stdio"] }
1387+language-servers = [ "rescript-language-server" ]
1324indent = { tab-width = 2, unit = " " }
1388indent = { tab-width = 2, unit = " " }
1325
1389
1326[[grammar]]
1390[[grammar]]
@@ -1336,7 +1400,7 @@ roots = ["rebar.config"]
1336shebangs = ["escript"]
1400shebangs = ["escript"]
1337comment-token = "%%"
1401comment-token = "%%"
1338indent = { tab-width = 4, unit = " " }
1402indent = { tab-width = 4, unit = " " }
1339−language-server = { command = "erlang_ls" }
1403+language-servers = [ "erlang-ls" ]
1340
1404
1341[language.auto-pairs]
1405[language.auto-pairs]
1342'(' = ')'
1406'(' = ')'
@@ -1357,7 +1421,7 @@ file-types = ["kt", "kts"]
1357roots = ["settings.gradle", "settings.gradle.kts"]
1421roots = ["settings.gradle", "settings.gradle.kts"]
1358comment-token = "//"
1422comment-token = "//"
1359indent = { tab-width = 4, unit = " " }
1423indent = { tab-width = 4, unit = " " }
1360−language-server = { command = "kotlin-language-server" }
1424+language-servers = [ "kotlin-language-server" ]
1361
1425
1362[[grammar]]
1426[[grammar]]
1363name = "kotlin"
1427name = "kotlin"
@@ -1367,11 +1431,12 @@ source = { git = "https://github.com/fwcd/tree-sitter-kotlin", rev = "a4f71eb9b8
1367name = "hcl"
1431name = "hcl"
1368scope = "source.hcl"
1432scope = "source.hcl"
1369injection-regex = "(hcl|tf|nomad)"
1433injection-regex = "(hcl|tf|nomad)"
1434+language-id = "terraform"
1370file-types = ["hcl", "tf", "nomad"]
1435file-types = ["hcl", "tf", "nomad"]
1371roots = []
1436roots = []
1372comment-token = "#"
1437comment-token = "#"
1373indent = { tab-width = 2, unit = " " }
1438indent = { tab-width = 2, unit = " " }
1374−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform" }
1439+language-servers = [ "terraform-ls" ]
1375auto-format = true
1440auto-format = true
1376
1441
1377[[grammar]]
1442[[grammar]]
@@ -1381,11 +1446,12 @@ source = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "3cb7
1381[[language]]
1446[[language]]
1382name = "tfvars"
1447name = "tfvars"
1383scope = "source.tfvars"
1448scope = "source.tfvars"
1449+language-id = "terraform-vars"
1384file-types = ["tfvars"]
1450file-types = ["tfvars"]
1385roots = []
1451roots = []
1386comment-token = "#"
1452comment-token = "#"
1387indent = { tab-width = 2, unit = " " }
1453indent = { tab-width = 2, unit = " " }
1388−language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform-vars" }
1454+language-servers = [ "terraform-ls" ]
1389auto-format = true
1455auto-format = true
1390grammar = "hcl"
1456grammar = "hcl"
1391
1457
@@ -1409,7 +1475,7 @@ file-types = ["sol"]
1409roots = []
1475roots = []
1410comment-token = "//"
1476comment-token = "//"
1411indent = { tab-width = 4, unit = " " }
1477indent = { tab-width = 4, unit = " " }
1412−language-server = { command = "solc", args = ["--lsp"] }
1478+language-servers = [ "solc" ]
1413
1479
1414[[grammar]]
1480[[grammar]]
1415name = "solidity"
1481name = "solidity"
@@ -1423,7 +1489,7 @@ file-types = ["gleam"]
1423roots = ["gleam.toml"]
1489roots = ["gleam.toml"]
1424comment-token = "//"
1490comment-token = "//"
1425indent = { tab-width = 2, unit = " " }
1491indent = { tab-width = 2, unit = " " }
1426−language-server = { command = "gleam", args = ["lsp"] }
1492+language-servers = [ "gleam" ]
1427
1493
1428[[grammar]]
1494[[grammar]]
1429name = "gleam"
1495name = "gleam"
@@ -1447,7 +1513,7 @@ file-types = ["robot", "resource"]
1447comment-token = "#"
1513comment-token = "#"
1448roots = []
1514roots = []
1449indent = { tab-width = 4, unit = " " }
1515indent = { tab-width = 4, unit = " " }
1450−language-server = { command = "robotframework_ls" }
1516+language-servers = [ "robotframework_ls" ]
1451
1517
1452[[grammar]]
1518[[grammar]]
1453name = "robot"
1519name = "robot"
@@ -1462,7 +1528,7 @@ shebangs = ["r", "R"]
1462roots = []
1528roots = []
1463comment-token = "#"
1529comment-token = "#"
1464indent = { tab-width = 2, unit = " " }
1530indent = { tab-width = 2, unit = " " }
1465−language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
1531+language-servers = [ "R" ]
1466
1532
1467[[grammar]]
1533[[grammar]]
1468name = "r"
1534name = "r"
@@ -1476,7 +1542,7 @@ file-types = ["rmd", "Rmd"]
1476roots = []
1542roots = []
1477indent = { tab-width = 2, unit = " " }
1543indent = { tab-width = 2, unit = " " }
1478grammar = "markdown"
1544grammar = "markdown"
1479−language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
1545+language-servers = [ "R" ]
1480
1546
1481[[language]]
1547[[language]]
1482name = "swift"
1548name = "swift"
@@ -1486,7 +1552,7 @@ file-types = ["swift"]
1486roots = [ "Package.swift" ]
1552roots = [ "Package.swift" ]
1487comment-token = "//"
1553comment-token = "//"
1488auto-format = true
1554auto-format = true
1489−language-server = { command = "sourcekit-lsp" }
1555+language-servers = [ "sourcekit-lsp" ]
1490
1556
1491[[grammar]]
1557[[grammar]]
1492name = "swift"
1558name = "swift"
@@ -1533,8 +1599,7 @@ injection-regex = "heex"
1533file-types = ["heex"]
1599file-types = ["heex"]
1534roots = ["mix.exs", "mix.lock"]
1600roots = ["mix.exs", "mix.lock"]
1535indent = { tab-width = 2, unit = " " }
1601indent = { tab-width = 2, unit = " " }
1536−language-server = { command = "elixir-ls" }
1602+language-servers = [ "elixir-ls" ]
1537−config = { elixirLS.dialyzerEnabled = false }
1538
1603
1539[[grammar]]
1604[[grammar]]
1540name = "heex"
1605name = "heex"
@@ -1605,7 +1670,7 @@ file-types = ["vala", "vapi"]
1605roots = []
1670roots = []
1606comment-token = "//"
1671comment-token = "//"
1607indent = { tab-width = 2, unit = " " }
1672indent = { tab-width = 2, unit = " " }
1608−language-server = { command = "vala-language-server" }
1673+language-servers = [ "vala-language-server" ]
1609
1674
1610[[grammar]]
1675[[grammar]]
1611name = "vala"
1676name = "vala"
@@ -1670,7 +1735,7 @@ auto-format = false
1670scope = "source.odin"
1735scope = "source.odin"
1671file-types = ["odin"]
1736file-types = ["odin"]
1672roots = ["ols.json"]
1737roots = ["ols.json"]
1673−language-server = { command = "ols", args = [] }
1738+language-servers = [ "ols" ]
1674comment-token = "//"
1739comment-token = "//"
1675indent = { tab-width = 4, unit = "\t" }
1740indent = { tab-width = 4, unit = "\t" }
1676
1741
@@ -1721,7 +1786,7 @@ scope = "source.v"
1721file-types = ["v", "vv", "vsh"]
1786file-types = ["v", "vv", "vsh"]
1722shebangs = ["v run"]
1787shebangs = ["v run"]
1723roots = ["v.mod"]
1788roots = ["v.mod"]
1724−language-server = { command = "v", args = ["ls"] }
1789+language-servers = [ "vlang-language-server" ]
1725auto-format = true
1790auto-format = true
1726comment-token = "//"
1791comment-token = "//"
1727indent = { tab-width = 4, unit = "\t" }
1792indent = { tab-width = 4, unit = "\t" }
@@ -1736,7 +1801,7 @@ scope = "source.verilog"
1736file-types = ["v", "vh", "sv", "svh"]
1801file-types = ["v", "vh", "sv", "svh"]
1737roots = []
1802roots = []
1738comment-token = "//"
1803comment-token = "//"
1739−language-server = { command = "svlangserver", args = [] }
1804+language-servers = [ "svlangserver" ]
1740indent = { tab-width = 2, unit = " " }
1805indent = { tab-width = 2, unit = " " }
1741injection-regex = "verilog"
1806injection-regex = "verilog"
1742
1807
@@ -1775,7 +1840,7 @@ injection-regex = "openscad"
1775file-types = ["scad"]
1840file-types = ["scad"]
1776roots = []
1841roots = []
1777comment-token = "//"
1842comment-token = "//"
1778−language-server = { command = "openscad-lsp", args = ["--stdio"] }
1843+language-servers = [ "openscad-lsp" ]
1779indent = { tab-width = 2, unit = "\t" }
1844indent = { tab-width = 2, unit = "\t" }
1780
1845
1781[[grammar]]
1846[[grammar]]
@@ -1789,7 +1854,7 @@ injection-regex = "prisma"
1789file-types = ["prisma"]
1854file-types = ["prisma"]
1790roots = ["package.json"]
1855roots = ["package.json"]
1791comment-token = "//"
1856comment-token = "//"
1792−language-server = { command = "prisma-language-server", args = ["--stdio"] }
1857+language-servers = [ "prisma-language-server" ]
1793indent = { tab-width = 2, unit = " " }
1858indent = { tab-width = 2, unit = " " }
1794
1859
1795[[grammar]]
1860[[grammar]]
@@ -1803,7 +1868,7 @@ injection-regex = "(clojure|clj|edn|boot)"
1803file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
1868file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
1804roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
1869roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
1805comment-token = ";"
1870comment-token = ";"
1806−language-server = { command = "clojure-lsp" }
1871+language-servers = [ "clojure-lsp" ]
1807indent = { tab-width = 2, unit = " " }
1872indent = { tab-width = 2, unit = " " }
1808
1873
1809[[grammar]]
1874[[grammar]]
@@ -1827,7 +1892,7 @@ file-types = ["elv"]
1827roots = []
1892roots = []
1828comment-token = "#"
1893comment-token = "#"
1829indent = { tab-width = 2, unit = " " }
1894indent = { tab-width = 2, unit = " " }
1830−language-server = { command = "elvish", args = ["-lsp"] }
1895+language-servers = [ "elvish" ]
1831grammar = "elvish"
1896grammar = "elvish"
1832
1897
1833[[grammar]]
1898[[grammar]]
@@ -1843,7 +1908,7 @@ shebangs = []
1843roots = []
1908roots = []
1844comment-token = "--"
1909comment-token = "--"
1845indent = { tab-width = 2, unit = " " }
1910indent = { tab-width = 2, unit = " " }
1846−language-server = { command = "idris2-lsp" }
1911+language-servers = [ "idris2-lsp" ]
1847
1912
1848[[language]]
1913[[language]]
1849name = "fortran"
1914name = "fortran"
@@ -1853,7 +1918,7 @@ file-types = ["f", "for", "f90", "f95", "f03"]
1853roots = ["fpm.toml"]
1918roots = ["fpm.toml"]
1854comment-token = "!"
1919comment-token = "!"
1855indent = { tab-width = 4, unit = " "}
1920indent = { tab-width = 4, unit = " "}
1856−language-server = { command = "fortls", args = ["--lowercase_intrinsics"] }
1921+language-servers = [ "fortls" ]
1857
1922
1858[[grammar]]
1923[[grammar]]
1859name = "fortran"
1924name = "fortran"
@@ -1880,7 +1945,7 @@ file-types = ["dot"]
1880roots = []
1945roots = []
1881comment-token = "//"
1946comment-token = "//"
1882indent = { tab-width = 4, unit = " " }
1947indent = { tab-width = 4, unit = " " }
1883−language-server = { command = "dot-language-server", args = ["--stdio"] }
1948+language-servers = [ "dot-language-server" ]
1884
1949
1885[[grammar]]
1950[[grammar]]
1886name = "dot"
1951name = "dot"
@@ -1894,7 +1959,7 @@ file-types = ["cue"]
1894roots = ["cue.mod"]
1959roots = ["cue.mod"]
1895auto-format = true
1960auto-format = true
1896comment-token = "//"
1961comment-token = "//"
1897−language-server = { command = "cuelsp" }
1962+language-servers = [ "cuelsp" ]
1898indent = { tab-width = 4, unit = "\t" }
1963indent = { tab-width = 4, unit = "\t" }
1899formatter = { command = "cue", args = ["fmt", "-"] }
1964formatter = { command = "cue", args = ["fmt", "-"] }
1900
1965
@@ -1910,7 +1975,7 @@ file-types = ["slint"]
1910roots = []
1975roots = []
1911comment-token = "//"
1976comment-token = "//"
1912indent = { tab-width = 4, unit = " " }
1977indent = { tab-width = 4, unit = " " }
1913−language-server = { command = "slint-lsp", args = [] }
1978+language-servers = [ "slint-lsp" ]
1914
1979
1915[[grammar]]
1980[[grammar]]
1916name = "slint"
1981name = "slint"
@@ -1962,7 +2027,7 @@ file-types = ["pas", "pp", "inc", "lpr", "lfm"]
1962roots = []
2027roots = []
1963comment-token = "//"
2028comment-token = "//"
1964indent = { tab-width = 2, unit = " " }
2029indent = { tab-width = 2, unit = " " }
1965−language-server = { command = "pasls", args = [] }
2030+language-servers = [ "pasls" ]
1966
2031
1967[[grammar]]
2032[[grammar]]
1968name = "pascal"
2033name = "pascal"
@@ -1987,7 +2052,7 @@ file-types = ["libsonnet", "jsonnet"]
1987roots = ["jsonnetfile.json"]
2052roots = ["jsonnetfile.json"]
1988comment-token = "//"
2053comment-token = "//"
1989indent = { tab-width = 2, unit = " " }
2054indent = { tab-width = 2, unit = " " }
1990−language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
2055+language-servers = [ "jsonnet-language-server" ]
1991
2056
1992[[grammar]]
2057[[grammar]]
1993name = "jsonnet"
2058name = "jsonnet"
@@ -2013,7 +2078,7 @@ file-types = ["bass"]
2013roots = []
2078roots = []
2014comment-token = ";"
2079comment-token = ";"
2015indent = { tab-width = 2, unit = " " }
2080indent = { tab-width = 2, unit = " " }
2016−language-server = { command = "bass", args = ["--lsp"] }
2081+language-servers = [ "bass" ]
2017
2082
2018[[grammar]]
2083[[grammar]]
2019name = "bass"
2084name = "bass"
@@ -2049,7 +2114,7 @@ roots = []
2049comment-token = "//"
2114comment-token = "//"
2050injection-regex = "d"
2115injection-regex = "d"
2051indent = { tab-width = 4, unit = " "}
2116indent = { tab-width = 4, unit = " "}
2052−language-server = { command = "serve-d" }
2117+language-servers = [ "serve-d" ]
2053formatter = { command = "dfmt" }
2118formatter = { command = "dfmt" }
2054
2119
2055[[grammar]]
2120[[grammar]]
@@ -2173,7 +2238,7 @@ roots = []
2173auto-format = true
2238auto-format = true
2174comment-token = "//"
2239comment-token = "//"
2175indent = { tab-width = 2, unit = " "}
2240indent = { tab-width = 2, unit = " "}
2176−language-server = { command = "bicep-langserver" }
2241+language-servers = [ "bicep-langserver" ]
2177
2242
2178[[grammar]]
2243[[grammar]]
2179name = "bicep"
2244name = "bicep"
@@ -2184,7 +2249,7 @@ name = "qml"
2184scope = "source.qml"
2249scope = "source.qml"
2185file-types = ["qml"]
2250file-types = ["qml"]
2186roots = []
2251roots = []
2187−language-server = { command = "qmlls" }
2252+language-servers = [ "qmlls" ]
2188indent = { tab-width = 4, unit = " " }
2253indent = { tab-width = 4, unit = " " }
2189grammar = "qmljs"
2254grammar = "qmljs"
2190
2255
@@ -2239,7 +2304,7 @@ file-types = ["dhall"]
2239roots = []
2304roots = []
2240comment-token = "--"
2305comment-token = "--"
2241indent = { tab-width = 2, unit = " " }
2306indent = { tab-width = 2, unit = " " }
2242−language-server = { command = "dhall-lsp-server" }
2307+language-servers = [ "dhall-lsp-server" ]
2243formatter = { command = "dhall" , args = ["format"] }
2308formatter = { command = "dhall" , args = ["format"] }
2244
2309
2245[[grammar]]
2310[[grammar]]
@@ -2401,7 +2466,7 @@ file-types = ["smithy"]
2401roots = ["smithy-build.json"]
2466roots = ["smithy-build.json"]
2402comment-token = "//"
2467comment-token = "//"
2403indent = { tab-width = 4, unit = " " }
2468indent = { tab-width = 4, unit = " " }
2404−language-server = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
2469+language-servers = [ "cs" ]
2405
2470
2406[[grammar]]
2471[[grammar]]
2407name = "smithy"
2472name = "smithy"
@@ -2413,7 +2478,7 @@ scope = "source.vhdl"
2413file-types = ["vhd", "vhdl"]
2478file-types = ["vhd", "vhdl"]
2414roots = []
2479roots = []
2415comment-token = "--"
2480comment-token = "--"
2416−language-server = { command = "vhdl_ls", args = [] }
2481+language-servers = [ "vhdl_ls" ]
2417indent = { tab-width = 2, unit = " " }
2482indent = { tab-width = 2, unit = " " }
2418injection-regex = "vhdl"
2483injection-regex = "vhdl"
2419
2484
@@ -2429,7 +2494,7 @@ injection-regex = "rego"
2429file-types = ["rego"]
2494file-types = ["rego"]
2430auto-format = true
2495auto-format = true
2431comment-token = "#"
2496comment-token = "#"
2432−language-server = { command = "regols" }
2497+language-servers = [ "regols" ]
2433grammar = "rego"
2498grammar = "rego"
2434
2499
2435[[grammar]]
2500[[grammar]]
@@ -2445,7 +2510,7 @@ shebangs = []
2445roots = []
2510roots = []
2446comment-token = "#"
2511comment-token = "#"
2447indent = { tab-width = 2, unit = " " }
2512indent = { tab-width = 2, unit = " " }
2448−language-server = { command = "nimlangserver" }
2513+language-servers = [ "nimlangserver" ]
2449
2514
2450[language.auto-pairs]
2515[language.auto-pairs]
2451'(' = ')'
2516'(' = ')'
@@ -2485,7 +2550,7 @@ name = "markdoc"
2485scope = "text.markdoc"
2550scope = "text.markdoc"
2486roots = []
2551roots = []
2487file-types = ["mdoc"]
2552file-types = ["mdoc"]
2488−language-server = { command = "markdoc-ls", args = ["--stdio"] }
2553+language-servers = [ "markdoc-ls" ]
2489
2554
2490[[grammar]]
2555[[grammar]]
2491name = "markdoc"
2556name = "markdoc"
@@ -2498,7 +2563,7 @@ injection-regex = "(cl|opencl)"
2498file-types = ["cl"]
2563file-types = ["cl"]
2499roots = []
2564roots = []
2500comment-token = "//"
2565comment-token = "//"
2501−language-server = { command = "clangd" }
2566+language-servers = [ "clangd" ]
2502
2567
2503[[grammar]]
2568[[grammar]]
2504name = "opencl"
2569name = "opencl"
xtask/src/docgen.rs+6−5
@@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
96 );
96 );
97 }
97 }
98 row.push(
98 row.push(
99+ lc.language_servers
100+ .iter()
99− lc.language_server
101+ .filter_map(|ls| config.language_server.get(ls.name()))
100− .as_ref()
101− .map(|s| s.command.clone())
102− .map(|c| md_mono(&c))
102+ .map(|s| md_mono(&s.command.clone()))
103− .unwrap_or_default(),
103+ .collect::<Vec<_>>()
104+ .join(", "),
104 );
105 );
105
106
106 md.push_str(&md_table_row(&row));
107 md.push_str(&md_table_row(&row));