Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple language servers per language #2507

Merged
merged 41 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
71551d3
Adds support for multiple language servers per language.
Philipp-M May 23, 2022
7d4f7eb
Fix 'WorkspaceConfiguration' request with empty configuration section…
Philipp-M Mar 27, 2023
74e21e1
Fix some lints/docgen hints
Philipp-M Mar 13, 2023
05583f8
Fix hardcoded offset_encoding
Philipp-M Mar 15, 2023
44b2b40
Fix issue with ltex-ls, filtering params is not what we want here
Philipp-M Mar 15, 2023
b6c60be
Remove unnecessary completion support check (likely an artifact)
Philipp-M Mar 15, 2023
4da6d8c
str instead of String
Philipp-M Mar 15, 2023
f9b0865
Fix sorting issues of the editor wide diagnostics and apply diagnosti…
Philipp-M Mar 17, 2023
19f88fc
Simplify Display implementation of LanguageServerFeature
Philipp-M Mar 17, 2023
dd2f747
Fix error messages when no language server is available
Philipp-M Mar 18, 2023
0637691
Use DoubleEndedIterator instead of collect to Vec for reversing
Philipp-M Mar 18, 2023
76b5cab
Refactored doc.language_servers and doc.language_servers_with_feature…
Philipp-M Mar 18, 2023
ec2f909
Simplify Display implementation for LanguageServerFeature
Philipp-M Mar 18, 2023
1122928
Add method doc.supports_language_server for better readability
Philipp-M Mar 18, 2023
9639f42
Refactor doc.shown_diagnostics to avoid an extra HashSet
Philipp-M Mar 18, 2023
d963050
Format/fix language docs a bit
Philipp-M Mar 18, 2023
60a6af1
Remove boilerplate in the goto methods by generically composing funct…
Philipp-M Mar 18, 2023
7d20740
Fix docgen and lsp-stop documentation
Philipp-M Mar 18, 2023
58c913c
Simplify 'lsp_stop' command
Philipp-M Mar 19, 2023
b1199c5
Remove symbol picker is_empty check
Philipp-M Mar 19, 2023
2eeac10
Refactor doc language servers to a HashMap, and the config to use a V…
Philipp-M Mar 19, 2023
1d5d5da
Remove offset_encoding in CompletionItem
Philipp-M Mar 19, 2023
8ab6d7b
Use let else instead of variable and fix some error messages
Philipp-M Mar 19, 2023
8ee5999
Optimize gutter diagnostics and simplify shown_diagnostics
Philipp-M Mar 19, 2023
451fe52
Filter out already seen language servers in requests that can be sent…
Philipp-M Mar 20, 2023
9d089c2
Fix docgen again
Philipp-M Mar 20, 2023
ff26208
Filter language servers also by capabilities in `doc.language_servers…
Philipp-M Mar 20, 2023
073000e
Maintain language servers TOML array order in `doc.language_servers`
Philipp-M Mar 20, 2023
93fd79a
Remove offset_encoding in CodeActionOrCommandItem, as it can be retri…
Philipp-M Mar 27, 2023
2a21b93
Fix crash with filtered diagnostics in gutter (e.g. when diagnostics …
Philipp-M Mar 28, 2023
3e4bac1
Fix lsp_restart across multiple different document scopes (language s…
Philipp-M Apr 2, 2023
5674850
Reduce boilerplate by 'use lsp::*' in Client::supports_feature, and r…
Philipp-M Apr 5, 2023
dcb0767
Reorder id generation for Clients to stay close to the old behavior
Philipp-M Apr 5, 2023
521cdec
Remove TODO comment in helix_lsp::Registry::restart and add doc-comme…
Philipp-M Apr 5, 2023
39b9a4b
Add function `Editor::language_server_by_id` and refactor/simplify re…
Philipp-M Apr 5, 2023
2b746ea
Some minor clarity/cosmetic improvements
Philipp-M Apr 5, 2023
656ee24
Simplify gutter diagnostics rendering by using partition_point instea…
Philipp-M Apr 5, 2023
f45bbf1
Apply all review suggestions (doc_id -> id, error message, unnecessar…
Philipp-M Apr 6, 2023
b6d0e26
Sort language servers table in languages.toml and rename language ser…
Philipp-M Apr 13, 2023
f8fa0d8
Clarify language-servers documentation for mergeable LSP features (`d…
Philipp-M Apr 14, 2023
2a512f7
Rebase cleanup/fixes and use lsp::CompletionItem in item_to_transacti…
Philipp-M May 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
| `:lsp-restart` | Restarts the language servers used by the current doc |
| `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
Expand Down
1 change: 1 addition & 0 deletions book/src/guides/adding_languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ below.
necessary configuration for the new language. For more information on
language configuration, refer to the
[language configuration section](../languages.md) of the documentation.
A new language server can be added by extending the `[language-server]` table in the same file.
2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation.
Expand Down
109 changes: 92 additions & 17 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
```toml
# in <config_dir>/helix/languages.toml

[language-server.mylang-lsp]
command = "mylang-lsp"

[[language]]
name = "rust"
auto-format = false
Expand All @@ -41,15 +44,16 @@ injection-regex = "mylang"
file-types = ["mylang", "myl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
```

These configuration keys are available:

| Key | Description |
| ---- | ----------- |
| `name` | The name of the language |
| `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 |
| `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 |
| `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. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
Expand All @@ -59,7 +63,7 @@ These configuration keys are available:
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| `comment-token` | The token to use as a comment-token |
| `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) |
| `language-server` | The Language Server to run. See the Language Server configuration section below. |
| `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) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit confusing before reading all the examples, because it can be two types but it wasn't written clearly. Like one can be { name = "efm-lsp-prettier", only-features = [ "format" ] } and one can be "typescript-language-server" and "typescript-language-server" means { name = "typescript-language-server" }, and the user have to go through two sections to understand this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see (and I have already seen how this wasn't clear for a few users here). Any suggestions how this could be improved?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I think it's pretty clear, personally. 🤷

| `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `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 |
Expand Down Expand Up @@ -92,31 +96,102 @@ with the following priorities:
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.

### Language Server configuration
## Language Server configuration

Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`

The `language-server` field takes the following keys:
For example:

| Key | Description |
| --- | ----------- |
| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `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 |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
```toml
[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
environment = { "ENV1" = "value1", "ENV2" = "value2" }

[language-server.efm-lsp-prettier]
command = "efm-langserver"

[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused by the duplication of the config option. In the provided example:

[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }   <<<<<<<<<<<<<<<<<<<<<<<<< old format?
environment = { "ENV1" = "value1", "ENV2" = "value2" }

[language-server.efm-lsp-prettier.config] <<<<<<<<<<<<<<<<<<<<<<<<< new format?
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }

Why is config = not considered legacy and removed? I would assume the new way to add a language server config would be:

[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
environment = { "ENV1" = "value1", "ENV2" = "value2" }

[language-server.mylang-lsp.config]
provideFormatter = true

Copy link
Member

@dead10ck dead10ck Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just TOML. Neither is old nor new; they are equivalent ways to express a subtable. See https://toml.io/en/v1.0.0#table

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, then I don't think we should show both as it is just confusing, I would go with:

[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
environment = { "ENV1" = "value1", "ENV2" = "value2" }

[language-server.mylang-lsp.config]
provideFormatter = true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not completely sure about the formatting of language server configuration yet TBH. See also #2507 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's confusing, personally. At least not more confusing than TOML is in general. It's just a style choice really. You use inline if you want more compact, and if it gets too big, you turn it into a section.


The top-level `config` field is used to configure the LSP initialization options. A `format`
sub-table within `config` can be used to pass extra formatting options to
[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).
These are the available options for a language server.

| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |

A `format` sub-table within `config` can be used to pass extra formatting options to
[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).
For example with typescript:

```toml
[[language]]
name = "typescript"
auto-format = true
[language-server.typescript-language-server]
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
```

### Configuring Language Servers for a language

The `language-servers` attribute in a language tells helix which language servers are used for this language.

They have to be defined in the `[language-server]` table as described in the previous section.

Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.

In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers.

For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default)
The language configuration for typescript could look like this:

```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```

or equivalent:

```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```

Each requested LSP feature is prioritized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
Philipp-M marked this conversation as resolved.
Show resolved Hide resolved
The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
If no `except-features` or `only-features` is given all features for the language server are enabled.
If a language server itself doesn't support a feature the next language server array entry will be tried (and so on).

The list of supported features is:

- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`

## Tree-sitter grammar configuration

The source for a language's tree-sitter grammar is specified in a `[[grammar]]`
Expand Down
1 change: 1 addition & 0 deletions helix-core/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct Diagnostic {
pub message: String,
pub severity: Option<Severity>,
pub code: Option<NumberOrString>,
pub language_server_id: usize,
pub tags: Vec<DiagnosticTag>,
pub source: Option<String>,
pub data: Option<serde_json::Value>,
Expand Down
Loading