Skip to content

Commit

Permalink
Adds support for multiple language servers per language.
Browse files Browse the repository at this point in the history
Language Servers are now configured in a separate table in `languages.toml`:

```toml
[langauge-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }

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

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

The language server for a language is configured like this (`typescript-language-server` is configured by default):

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

or equivalent:

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

Each requested LSP feature is priorized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).

If no `except-features` or `only-features` is given all features for the language server are enabled, as long as the language server supports these. If it doesn't the next language server which supports the feature is tried.

The list of supported features are:

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

Another side-effect/difference that comes with this PR, is that only one language server instance is started if different languages use the same language server.
  • Loading branch information
Philipp-M committed May 18, 2023
1 parent 7f5940b commit 71551d3
Show file tree
Hide file tree
Showing 22 changed files with 1,555 additions and 1,058 deletions.
2 changes: 1 addition & 1 deletion book/src/generated/typable-cmd.md
Expand Up @@ -50,7 +50,7 @@
| `: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-restart` | Restarts the language servers used by the currently opened file |
| `:lsp-stop` | Stops the Language Server that is in use 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. |
Expand Down
1 change: 1 addition & 0 deletions book/src/guides/adding_languages.md
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
104 changes: 87 additions & 17 deletions book/src/languages.md
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) |
| `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,97 @@ 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`

For example:

```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 } ] }
```

The `language-server` field takes the following keys:
These are the available options for a language server.

| 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" }` |
| 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" }` |

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).
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 priorized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
If no `except-features` or `only-features` is given all features for the language server are enabled.
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 are:

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

## 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
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
113 changes: 102 additions & 11 deletions helix-core/src/syntax.rs
Expand Up @@ -17,7 +17,7 @@ use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, VecDeque},
fmt,
fmt::{self, Display},
hash::{Hash, Hasher},
mem::{replace, transmute},
path::{Path, PathBuf},
Expand Down Expand Up @@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
}

impl Default for Configuration {
Expand All @@ -75,7 +78,10 @@ impl Default for Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String, // c-sharp, rust
pub language_id: String, // c-sharp, rust, tsx
#[serde(rename = "language-id")]
// see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
pub scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)]
Expand All @@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,

#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,

#[serde(default)]
pub auto_format: bool,

Expand All @@ -107,8 +110,8 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
#[serde(skip_serializing_if = "Option::is_none")]
pub language_server: Option<LanguageServerConfiguration>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub language_servers: Vec<LanguageServerFeatureConfiguration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>,

Expand Down Expand Up @@ -208,6 +211,68 @@ impl<'de> Deserialize<'de> for FileType {
}
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature {
Format,
GotoDeclaration,
GotoDefinition,
GotoTypeDefinition,
GotoReference,
GotoImplementation,
// Goto, use bitflags, combining previous Goto members?
SignatureHelp,
Hover,
DocumentHighlight,
Completion,
CodeAction,
WorkspaceCommand,
DocumentSymbols,
WorkspaceSymbols,
// Symbols, use bitflags, see above?
Diagnostics,
RenameSymbol,
InlayHints,
}

impl Display for LanguageServerFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LanguageServerFeature::Format => write!(f, "format"),
LanguageServerFeature::GotoDeclaration => write!(f, "goto-declaration"),
LanguageServerFeature::GotoDefinition => write!(f, "goto-definition"),
LanguageServerFeature::GotoTypeDefinition => write!(f, "goto-type-definition"),
LanguageServerFeature::GotoReference => write!(f, "goto-type-definition"),
LanguageServerFeature::GotoImplementation => write!(f, "goto-implementation"),
LanguageServerFeature::SignatureHelp => write!(f, "signature-help"),
LanguageServerFeature::Hover => write!(f, "hover"),
LanguageServerFeature::DocumentHighlight => write!(f, "document-highlight"),
LanguageServerFeature::Completion => write!(f, "completion"),
LanguageServerFeature::CodeAction => write!(f, "code-action"),
LanguageServerFeature::WorkspaceCommand => write!(f, "workspace-command"),
LanguageServerFeature::DocumentSymbols => write!(f, "document-symbols"),
LanguageServerFeature::WorkspaceSymbols => write!(f, "workspace-symbols"),
LanguageServerFeature::Diagnostics => write!(f, "diagnostics"),
LanguageServerFeature::RenameSymbol => write!(f, "rename-symbol"),
LanguageServerFeature::InlayHints => write!(f, "inlay-hints"),
}
}
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
pub enum LanguageServerFeatureConfiguration {
#[serde(rename_all = "kebab-case")]
Features {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
only_features: Vec<LanguageServerFeature>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
except_features: Vec<LanguageServerFeature>,
name: String,
},
Simple(String),
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
Expand All @@ -217,9 +282,10 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")]
pub timeout: u64,
pub language_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -584,6 +650,15 @@ pub struct SoftWrap {
pub wrap_at_text_width: Option<bool>,
}

impl LanguageServerFeatureConfiguration {
pub fn name(&self) -> &String {
match self {
LanguageServerFeatureConfiguration::Simple(name) => name,
LanguageServerFeatureConfiguration::Features { name, .. } => name,
}
}
}

// Expose loader as Lazy<> global since it's always static?

#[derive(Debug)]
Expand All @@ -594,13 +669,16 @@ pub struct Loader {
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>,

language_server_configs: HashMap<String, LanguageServerConfiguration>,

scopes: ArcSwap<Vec<String>>,
}

impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
Expand Down Expand Up @@ -725,6 +803,10 @@ impl Loader {
self.language_configs.iter()
}

pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
&self.language_server_configs
}

pub fn set_scopes(&self, scopes: Vec<String>) {
self.scopes.store(Arc::new(scopes));

Expand Down Expand Up @@ -2370,7 +2452,10 @@ mod test {
"#,
);

let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap();

let query = Query::new(language, query_str).unwrap();
Expand Down Expand Up @@ -2429,7 +2514,10 @@ mod test {
.map(String::from)
.collect();

let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});

let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
Expand Down Expand Up @@ -2532,7 +2620,10 @@ mod test {
) {
let source = Rope::from_str(source);

let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language(language_name).unwrap();

let config = HighlightConfiguration::new(language, "", "", "").unwrap();
Expand Down

0 comments on commit 71551d3

Please sign in to comment.