Merge pull request #2507 from Philipp-M/multiple-language-servers

Add support for multiple language servers per language
pull/7072/head
Blaž Hrastnik 1 year ago committed by GitHub
commit 53f47bc477
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -50,8 +50,8 @@
| `:reload-all` | Discard changes and reload all documents from the source files. | | `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. | | `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker | | `: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 current doc |
| `:lsp-stop` | Stops the Language Server that is in use 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. | | `: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-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. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |

@ -9,6 +9,7 @@ below.
necessary configuration for the new language. For more information on necessary configuration for the new language. For more information on
language configuration, refer to the language configuration, refer to the
[language configuration section](../languages.md) of the documentation. [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 2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation. [Language Support](../lang-support.md) documentation.

@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file:
```toml ```toml
# in <config_dir>/helix/languages.toml # in <config_dir>/helix/languages.toml
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]] [[language]]
name = "rust" name = "rust"
auto-format = false auto-format = false
@ -41,8 +44,8 @@ injection-regex = "mylang"
file-types = ["mylang", "myl"] file-types = ["mylang", "myl"]
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] } formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
``` ```
These configuration keys are available: These configuration keys are available:
@ -50,6 +53,7 @@ These configuration keys are available:
| Key | Description | | Key | Description |
| ---- | ----------- | | ---- | ----------- |
| `name` | The name of the language | | `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 | | `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. | | `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. | | `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:
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) | | `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 | | `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) | | `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 | | `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `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 | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
@ -92,31 +96,102 @@ with the following priorities:
replaced at runtime with the appropriate path separator for the operating replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows. 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 | ```toml
| --- | ----------- | [language-server.mylang-lsp]
| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` | command = "mylang-lsp"
| `args` | A list of arguments to pass to the language server binary | args = ["--stdio"]
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | config = { provideFormatter = true }
| `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 = { "ENV1" = "value1", "ENV2" = "value2" }
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "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 top-level `config` field is used to configure the LSP initialization options. A `format` These are the available options for a language server.
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). | 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: For example with typescript:
```toml ```toml
[[language]] [language-server.typescript-language-server]
name = "typescript"
auto-format = true
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix. # 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 } } 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`).
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 ## Tree-sitter grammar configuration
The source for a language's tree-sitter grammar is specified in a `[[grammar]]` The source for a language's tree-sitter grammar is specified in a `[[grammar]]`

@ -43,6 +43,7 @@ pub struct Diagnostic {
pub message: String, pub message: String,
pub severity: Option<Severity>, pub severity: Option<Severity>,
pub code: Option<NumberOrString>, pub code: Option<NumberOrString>,
pub language_server_id: usize,
pub tags: Vec<DiagnosticTag>, pub tags: Vec<DiagnosticTag>,
pub source: Option<String>, pub source: Option<String>,
pub data: Option<serde_json::Value>, pub data: Option<serde_json::Value>,

@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
collections::{HashMap, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt, fmt::{self, Display},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
mem::{replace, transmute}, mem::{replace, transmute},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -26,7 +26,7 @@ use std::{
}; };
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize}; use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file}; use helix_loader::grammar::{get_language, load_runtime_file};
@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
} }
impl Default for Configuration { impl Default for Configuration {
@ -75,7 +78,10 @@ impl Default for Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[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 scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc> pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)] #[serde(default)]
@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
pub text_width: Option<usize>, pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>, pub soft_wrap: Option<SoftWrap>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default)] #[serde(default)]
pub auto_format: bool, pub auto_format: bool,
@ -107,8 +110,13 @@ pub struct LanguageConfiguration {
#[serde(skip)] #[serde(skip)]
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>, pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
#[serde(skip_serializing_if = "Option::is_none")] #[serde(
pub language_server: Option<LanguageServerConfiguration>, default,
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_lang_features",
deserialize_with = "deserialize_lang_features"
)]
pub language_servers: Vec<LanguageServerFeatures>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>, pub indent: Option<IndentationConfiguration>,
@ -208,6 +216,133 @@ impl<'de> Deserialize<'de> for FileType {
} }
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[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 {
use LanguageServerFeature::*;
let feature = match self {
Format => "format",
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
DocumentHighlight => "document-highlight",
Completion => "completion",
CodeAction => "code-action",
WorkspaceCommand => "workspace-command",
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
};
write!(f, "{feature}",)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
enum LanguageServerFeatureConfiguration {
#[serde(rename_all = "kebab-case")]
Features {
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
only_features: HashSet<LanguageServerFeature>,
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
except_features: HashSet<LanguageServerFeature>,
name: String,
},
Simple(String),
}
#[derive(Debug, Default)]
pub struct LanguageServerFeatures {
pub name: String,
pub only: HashSet<LanguageServerFeature>,
pub excluded: HashSet<LanguageServerFeature>,
}
impl LanguageServerFeatures {
pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
(self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
}
}
fn deserialize_lang_features<'de, D>(
deserializer: D,
) -> Result<Vec<LanguageServerFeatures>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
let res = raw
.into_iter()
.map(|config| match config {
LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
name,
..Default::default()
},
LanguageServerFeatureConfiguration::Features {
only_features,
except_features,
name,
} => LanguageServerFeatures {
name,
only: only_features,
excluded: except_features,
},
})
.collect();
Ok(res)
}
fn serialize_lang_features<S>(
map: &Vec<LanguageServerFeatures>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut serializer = serializer.serialize_seq(Some(map.len()))?;
for features in map {
let features = if features.only.is_empty() && features.excluded.is_empty() {
LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
} else {
LanguageServerFeatureConfiguration::Features {
only_features: features.only.clone(),
except_features: features.excluded.clone(),
name: features.name.to_owned(),
}
};
serializer.serialize_element(&features)?;
}
serializer.end()
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration { pub struct LanguageServerConfiguration {
@ -217,9 +352,10 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")] #[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>, pub environment: HashMap<String, String>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]
pub timeout: u64, pub timeout: u64,
pub language_id: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -594,6 +730,8 @@ pub struct Loader {
language_config_ids_by_suffix: HashMap<String, usize>, language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>, language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
scopes: ArcSwap<Vec<String>>, scopes: ArcSwap<Vec<String>>,
} }
@ -601,6 +739,7 @@ impl Loader {
pub fn new(config: Configuration) -> Self { pub fn new(config: Configuration) -> Self {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(), language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(), language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(), language_config_ids_by_shebang: HashMap::new(),
@ -725,6 +864,10 @@ impl Loader {
self.language_configs.iter() 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>) { pub fn set_scopes(&self, scopes: Vec<String>) {
self.scopes.store(Arc::new(scopes)); self.scopes.store(Arc::new(scopes));
@ -2370,7 +2513,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 language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap(); let query = Query::new(language, query_str).unwrap();
@ -2429,7 +2575,10 @@ mod test {
.map(String::from) .map(String::from)
.collect(); .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 language = get_language("rust").unwrap();
let config = HighlightConfiguration::new( let config = HighlightConfiguration::new(
@ -2532,7 +2681,10 @@ mod test {
) { ) {
let source = Rope::from_str(source); 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 language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();

@ -4,7 +4,7 @@ use crate::{
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{find_workspace, path, ChangeSet, Rope}; use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH}; use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{ use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf, notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
id: usize, id: usize,
name: String,
_process: Child, _process: Child,
server_tx: UnboundedSender<Payload>, server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64, request_counter: AtomicU64,
@ -166,8 +167,7 @@ impl Client {
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
} }
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity, clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments)]
pub fn start( pub fn start(
cmd: &str, cmd: &str,
args: &[String], args: &[String],
@ -176,6 +176,7 @@ impl Client {
root_markers: &[String], root_markers: &[String],
manual_roots: &[PathBuf], manual_roots: &[PathBuf],
id: usize, id: usize,
name: String,
req_timeout: u64, req_timeout: u64,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> { ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@ -200,7 +201,7 @@ impl Client {
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr")); let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (server_rx, server_tx, initialize_notify) = let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id); Transport::start(reader, writer, stderr, id, name.clone());
let (workspace, workspace_is_cwd) = find_workspace(); let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace); let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace( let root = find_lsp_workspace(
@ -225,6 +226,7 @@ impl Client {
let client = Self { let client = Self {
id, id,
name,
_process: process, _process: process,
server_tx, server_tx,
request_counter: AtomicU64::new(0), request_counter: AtomicU64::new(0),
@ -240,6 +242,10 @@ impl Client {
Ok((client, server_rx, initialize_notify)) Ok((client, server_rx, initialize_notify))
} }
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> usize { pub fn id(&self) -> usize {
self.id self.id
} }
@ -270,6 +276,87 @@ impl Client {
.expect("language server not yet initialized!") .expect("language server not yet initialized!")
} }
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
let capabilities = self.capabilities();
use lsp::*;
match feature {
LanguageServerFeature::Format => matches!(
capabilities.document_formatting_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoDeclaration => matches!(
capabilities.declaration_provider,
Some(
DeclarationCapability::Simple(true)
| DeclarationCapability::RegistrationOptions(_)
| DeclarationCapability::Options(_),
)
),
LanguageServerFeature::GotoDefinition => matches!(
capabilities.definition_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoTypeDefinition => matches!(
capabilities.type_definition_provider,
Some(
TypeDefinitionProviderCapability::Simple(true)
| TypeDefinitionProviderCapability::Options(_),
)
),
LanguageServerFeature::GotoReference => matches!(
capabilities.references_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoImplementation => matches!(
capabilities.implementation_provider,
Some(
ImplementationProviderCapability::Simple(true)
| ImplementationProviderCapability::Options(_),
)
),
LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
LanguageServerFeature::Hover => matches!(
capabilities.hover_provider,
Some(HoverProviderCapability::Simple(true) | HoverProviderCapability::Options(_),)
),
LanguageServerFeature::DocumentHighlight => matches!(
capabilities.document_highlight_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
LanguageServerFeature::CodeAction => matches!(
capabilities.code_action_provider,
Some(
CodeActionProviderCapability::Simple(true)
| CodeActionProviderCapability::Options(_),
)
),
LanguageServerFeature::WorkspaceCommand => {
capabilities.execute_command_provider.is_some()
}
LanguageServerFeature::DocumentSymbols => matches!(
capabilities.document_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::WorkspaceSymbols => matches!(
capabilities.workspace_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
),
LanguageServerFeature::InlayHints => matches!(
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
}
}
pub fn offset_encoding(&self) -> OffsetEncoding { pub fn offset_encoding(&self) -> OffsetEncoding {
self.capabilities() self.capabilities()
.position_encoding .position_encoding
@ -1295,21 +1382,13 @@ impl Client {
Some(self.call::<lsp::request::CodeActionRequest>(params)) Some(self.call::<lsp::request::CodeActionRequest>(params))
} }
pub fn supports_rename(&self) -> bool {
let capabilities = self.capabilities.get().unwrap();
matches!(
capabilities.rename_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
}
pub fn rename_symbol( pub fn rename_symbol(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
new_name: String, new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> { ) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
if !self.supports_rename() { if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None; return None;
} }

@ -12,24 +12,21 @@ pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll; use futures_util::stream::select_all::SelectAll;
use helix_core::{ use helix_core::{
path, path,
syntax::{LanguageConfiguration, LanguageServerConfiguration}, syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
}; };
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::Arc,
atomic::{AtomicUsize, Ordering},
Arc,
},
}; };
use thiserror::Error; use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String; pub type LanguageServerName = String;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@ -49,7 +46,7 @@ pub enum Error {
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OffsetEncoding { pub enum OffsetEncoding {
/// UTF-8 code units aka bytes /// UTF-8 code units aka bytes
Utf8, Utf8,
@ -624,23 +621,18 @@ impl Notification {
#[derive(Debug)] #[derive(Debug)]
pub struct Registry { pub struct Registry {
inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>, inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
syn_loader: Arc<helix_core::syntax::Loader>,
counter: AtomicUsize, counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>, pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
} }
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
impl Registry { impl Registry {
pub fn new() -> Self { pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
Self { Self {
inner: HashMap::new(), inner: HashMap::new(),
counter: AtomicUsize::new(0), syn_loader,
counter: 0,
incoming: SelectAll::new(), incoming: SelectAll::new(),
} }
} }
@ -649,65 +641,92 @@ impl Registry {
self.inner self.inner
.values() .values()
.flatten() .flatten()
.find(|(client_id, _)| client_id == &id) .find(|client| client.id() == id)
.map(|(_, client)| client.as_ref()) .map(|client| &**client)
} }
pub fn remove_by_id(&mut self, id: usize) { pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, clients| { self.inner.retain(|_, language_servers| {
clients.retain(|&(client_id, _)| client_id != id); language_servers.retain(|ls| id != ls.id());
!clients.is_empty() !language_servers.is_empty()
}) });
} }
fn start_client(
&mut self,
name: String,
ls_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Arc<Client>> {
let config = self
.syn_loader
.language_server_configs()
.get(&name)
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
let id = self.counter;
self.counter += 1;
let NewClient(client, incoming) = start_client(
id,
name,
ls_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
}
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
/// as it could be that language servers of these documents were stopped by this method.
/// See helix_view::editor::Editor::refresh_language_servers
pub fn restart( pub fn restart(
&mut self, &mut self,
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<Vec<Arc<Client>>> {
let config = match &language_config.language_server { language_config
Some(config) => config, .language_servers
None => return Ok(None), .iter()
}; .filter_map(|LanguageServerFeatures { name, .. }| {
if self.inner.contains_key(name) {
let scope = language_config.scope.clone(); let client = match self.start_client(
name.clone(),
match self.inner.entry(scope) { language_config,
Entry::Vacant(_) => Ok(None), doc_path,
Entry::Occupied(mut entry) => { root_dirs,
// initialize a new client enable_snippets,
let id = self.counter.fetch_add(1, Ordering::Relaxed); ) {
Ok(client) => client,
let NewClientResult(client, incoming) = start_client( error => return Some(error),
id, };
language_config, let old_clients = self
config, .inner
doc_path, .insert(name.clone(), vec![client.clone()])
root_dirs, .unwrap();
enable_snippets,
)?; for old_client in old_clients {
self.incoming.push(UnboundedReceiverStream::new(incoming)); tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
let old_clients = entry.insert(vec![(id, client.clone())]); });
}
for (_, old_client) in old_clients {
tokio::spawn(async move { Some(Ok(client))
let _ = old_client.force_shutdown().await; } else {
}); None
} }
})
Ok(Some(client)) .collect()
}
}
} }
pub fn stop(&mut self, language_config: &LanguageConfiguration) { pub fn stop(&mut self, name: &str) {
let scope = language_config.scope.clone(); if let Some(clients) = self.inner.remove(name) {
for client in clients {
if let Some(clients) = self.inner.remove(&scope) {
for (_, client) in clients {
tokio::spawn(async move { tokio::spawn(async move {
let _ = client.force_shutdown().await; let _ = client.force_shutdown().await;
}); });
@ -721,37 +740,34 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<Option<Arc<Client>>> { ) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
let config = match &language_config.language_server { language_config
Some(config) => config, .language_servers
None => return Ok(None), .iter()
}; .map(|LanguageServerFeatures { name, .. }| {
if let Some(clients) = self.inner.get(name) {
let clients = self.inner.entry(language_config.scope.clone()).or_default(); if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
// check if we already have a client for this documents root that we can reuse client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| { }) {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) return Ok((name.to_owned(), client.clone()));
}) { }
return Ok(Some(client.1.clone())); }
} let client = self.start_client(
// initialize a new client name.clone(),
let id = self.counter.fetch_add(1, Ordering::Relaxed); language_config,
doc_path,
let NewClientResult(client, incoming) = start_client( root_dirs,
id, enable_snippets,
language_config, )?;
config, let clients = self.inner.entry(name.clone()).or_default();
doc_path, clients.push(client.clone());
root_dirs, Ok((name.clone(), client))
enable_snippets, })
)?; .collect()
clients.push((id, client.clone()));
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(Some(client))
} }
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> { pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().flatten().map(|(_, client)| client) self.inner.values().flatten()
} }
} }
@ -833,26 +849,28 @@ impl LspProgressMap {
} }
} }
struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>); struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense. /// it is only called when it makes sense.
fn start_client( fn start_client(
id: usize, id: usize,
name: String,
config: &LanguageConfiguration, config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration, ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>, doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf], root_dirs: &[PathBuf],
enable_snippets: bool, enable_snippets: bool,
) -> Result<NewClientResult> { ) -> Result<NewClient> {
let (client, incoming, initialize_notify) = Client::start( let (client, incoming, initialize_notify) = Client::start(
&ls_config.command, &ls_config.command,
&ls_config.args, &ls_config.args,
config.config.clone(), ls_config.config.clone(),
ls_config.environment.clone(), ls_config.environment.clone(),
&config.roots, &config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs), config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id, id,
name,
ls_config.timeout, ls_config.timeout,
doc_path, doc_path,
)?; )?;
@ -886,7 +904,7 @@ fn start_client(
initialize_notify.notify_one(); initialize_notify.notify_one();
}); });
Ok(NewClientResult(client, incoming)) Ok(NewClient(client, incoming))
} }
/// Find an LSP workspace of a file using the following mechanism: /// Find an LSP workspace of a file using the following mechanism:

@ -38,6 +38,7 @@ enum ServerMessage {
#[derive(Debug)] #[derive(Debug)]
pub struct Transport { pub struct Transport {
id: usize, id: usize,
name: String,
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>, pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
} }
@ -47,6 +48,7 @@ impl Transport {
server_stdin: BufWriter<ChildStdin>, server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>, server_stderr: BufReader<ChildStderr>,
id: usize, id: usize,
name: String,
) -> ( ) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>, UnboundedSender<Payload>,
@ -58,6 +60,7 @@ impl Transport {
let transport = Self { let transport = Self {
id, id,
name,
pending_requests: Mutex::new(HashMap::default()), pending_requests: Mutex::new(HashMap::default()),
}; };
@ -83,6 +86,7 @@ impl Transport {
async fn recv_server_message( async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send), reader: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String, buffer: &mut String,
language_server_name: &str,
) -> Result<ServerMessage> { ) -> Result<ServerMessage> {
let mut content_length = None; let mut content_length = None;
loop { loop {
@ -124,7 +128,7 @@ impl Transport {
reader.read_exact(&mut content).await?; reader.read_exact(&mut content).await?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?; let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
info!("<- {}", msg); info!("{language_server_name} <- {msg}");
// try parsing as output (server response) or call (server request) // try parsing as output (server response) or call (server request)
let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg); let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@ -135,12 +139,13 @@ impl Transport {
async fn recv_server_error( async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send), err: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String, buffer: &mut String,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
buffer.truncate(0); buffer.truncate(0);
if err.read_line(buffer).await? == 0 { if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed); return Err(Error::StreamClosed);
}; };
error!("err <- {:?}", buffer); error!("{language_server_name} err <- {buffer:?}");
Ok(()) Ok(())
} }
@ -162,15 +167,17 @@ impl Transport {
Payload::Notification(value) => serde_json::to_string(&value)?, Payload::Notification(value) => serde_json::to_string(&value)?,
Payload::Response(error) => serde_json::to_string(&error)?, Payload::Response(error) => serde_json::to_string(&error)?,
}; };
self.send_string_to_server(server_stdin, json).await self.send_string_to_server(server_stdin, json, &self.name)
.await
} }
async fn send_string_to_server( async fn send_string_to_server(
&self, &self,
server_stdin: &mut BufWriter<ChildStdin>, server_stdin: &mut BufWriter<ChildStdin>,
request: String, request: String,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
info!("-> {}", request); info!("{language_server_name} -> {request}");
// send the headers // send the headers
server_stdin server_stdin
@ -189,9 +196,13 @@ impl Transport {
&self, &self,
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>, client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
msg: ServerMessage, msg: ServerMessage,
language_server_name: &str,
) -> Result<()> { ) -> Result<()> {
match msg { match msg {
ServerMessage::Output(output) => self.process_request_response(output).await?, ServerMessage::Output(output) => {
self.process_request_response(output, language_server_name)
.await?
}
ServerMessage::Call(call) => { ServerMessage::Call(call) => {
client_tx client_tx
.send((self.id, call)) .send((self.id, call))
@ -202,14 +213,18 @@ impl Transport {
Ok(()) Ok(())
} }
async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> { async fn process_request_response(
&self,
output: jsonrpc::Output,
language_server_name: &str,
) -> Result<()> {
let (id, result) = match output { let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => { jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result); info!("{language_server_name} <- {}", result);
(id, Ok(result)) (id, Ok(result))
} }
jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => { jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
error!("<- {}", error); error!("{language_server_name} <- {error}");
(id, Err(error.into())) (id, Err(error.into()))
} }
}; };
@ -240,12 +255,17 @@ impl Transport {
) { ) {
let mut recv_buffer = String::new(); let mut recv_buffer = String::new();
loop { loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await { match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
.await
{
Ok(msg) => { Ok(msg) => {
match transport.process_server_message(&client_tx, msg).await { match transport
.process_server_message(&client_tx, msg, &transport.name)
.await
{
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
}; };
@ -270,7 +290,7 @@ impl Transport {
params: jsonrpc::Params::None, params: jsonrpc::Params::None,
})); }));
match transport match transport
.process_server_message(&client_tx, notification) .process_server_message(&client_tx, notification, &transport.name)
.await .await
{ {
Ok(_) => {} Ok(_) => {}
@ -281,20 +301,22 @@ impl Transport {
break; break;
} }
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
} }
} }
} }
async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) { async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
let mut recv_buffer = String::new(); let mut recv_buffer = String::new();
loop { loop {
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await { match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
.await
{
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
break; break;
} }
} }
@ -348,10 +370,11 @@ impl Transport {
method: lsp_types::notification::Initialized::METHOD.to_string(), method: lsp_types::notification::Initialized::METHOD.to_string(),
params: jsonrpc::Params::None, params: jsonrpc::Params::None,
})); }));
match transport.process_server_message(&client_tx, notification).await { let language_server_name = &transport.name;
match transport.process_server_message(&client_tx, notification, language_server_name).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{language_server_name} err: <- {err:?}");
} }
} }
@ -361,7 +384,7 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await { match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{language_server_name} err: <- {err:?}");
} }
} }
} }
@ -380,7 +403,7 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await { match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("err: <- {:?}", err); error!("{} err: <- {err:?}", transport.name);
} }
} }
} }

@ -30,6 +30,7 @@ use crate::{
use log::{debug, error, warn}; use log::{debug, error, warn};
use std::{ use std::{
collections::btree_map::Entry,
io::{stdin, stdout}, io::{stdin, stdout},
path::Path, path::Path,
sync::Arc, sync::Arc,
@ -564,7 +565,7 @@ impl Application {
let doc = doc_mut!(self.editor, &doc_save_event.doc_id); let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id(); let id = doc.id();
doc.detect_language(loader); doc.detect_language(loader);
let _ = self.editor.refresh_language_server(id); self.editor.refresh_language_servers(id);
} }
// TODO: fix being overwritten by lsp // TODO: fix being overwritten by lsp
@ -662,6 +663,18 @@ impl Application {
) { ) {
use helix_lsp::{Call, MethodCall, Notification}; use helix_lsp::{Call, MethodCall, Notification};
macro_rules! language_server {
() => {
match self.editor.language_server_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
}
};
}
match call { match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) { let notification = match Notification::parse(&method, params) {
@ -677,14 +690,7 @@ impl Application {
match notification { match notification {
Notification::Initialized => { Notification::Initialized => {
let language_server = let language_server = language_server!();
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
// Trigger a workspace/didChangeConfiguration notification after initialization. // Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's // This might not be required by the spec but Neovim does this as well, so it's
@ -693,9 +699,10 @@ impl Application {
tokio::spawn(language_server.did_change_configuration(config.clone())); tokio::spawn(language_server.did_change_configuration(config.clone()));
} }
let docs = self.editor.documents().filter(|doc| { let docs = self
doc.language_server().map(|server| server.id()) == Some(server_id) .editor
}); .documents()
.filter(|doc| doc.supports_language_server(server_id));
// trigger textDocument/didOpen for docs that are already open // trigger textDocument/didOpen for docs that are already open
for doc in docs { for doc in docs {
@ -715,7 +722,7 @@ impl Application {
)); ));
} }
} }
Notification::PublishDiagnostics(mut params) => { Notification::PublishDiagnostics(params) => {
let path = match params.uri.to_file_path() { let path = match params.uri.to_file_path() {
Ok(path) => path, Ok(path) => path,
Err(_) => { Err(_) => {
@ -723,6 +730,7 @@ impl Application {
return; return;
} }
}; };
let offset_encoding = language_server!().offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| { let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version { if let Some(version) = params.version {
if version != doc.version() { if version != doc.version() {
@ -745,18 +753,11 @@ impl Application {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*}; use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity; use lsp::DiagnosticSeverity;
let language_server = if let Some(language_server) = doc.language_server() {
language_server
} else {
log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
return None;
};
// TODO: convert inside server // TODO: convert inside server
let start = if let Some(start) = lsp_pos_to_pos( let start = if let Some(start) = lsp_pos_to_pos(
text, text,
diagnostic.range.start, diagnostic.range.start,
language_server.offset_encoding(), offset_encoding,
) { ) {
start start
} else { } else {
@ -764,11 +765,9 @@ impl Application {
return None; return None;
}; };
let end = if let Some(end) = lsp_pos_to_pos( let end = if let Some(end) =
text, lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
diagnostic.range.end, {
language_server.offset_encoding(),
) {
end end
} else { } else {
log::warn!("lsp position out of bounds - {:?}", diagnostic); log::warn!("lsp position out of bounds - {:?}", diagnostic);
@ -807,14 +806,19 @@ impl Application {
None => None, None => None,
}; };
let tags = if let Some(ref tags) = diagnostic.tags { let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags.iter().filter_map(|tag| { let new_tags = tags
match *tag { .iter()
lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated), .filter_map(|tag| match *tag {
lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary), lsp::DiagnosticTag::DEPRECATED => {
_ => None Some(DiagnosticTag::Deprecated)
} }
}).collect(); lsp::DiagnosticTag::UNNECESSARY => {
Some(DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();
new_tags new_tags
} else { } else {
@ -830,25 +834,40 @@ impl Application {
tags, tags,
source: diagnostic.source.clone(), source: diagnostic.source.clone(),
data: diagnostic.data.clone(), data: diagnostic.data.clone(),
language_server_id: server_id,
}) })
}) })
.collect(); .collect();
doc.set_diagnostics(diagnostics); doc.replace_diagnostics(diagnostics, server_id);
} }
// Sort diagnostics first by severity and then by line numbers. let mut diagnostics = params
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
.diagnostics .diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start)); .into_iter()
.map(|d| (d, server_id))
.collect();
// Insert the original lsp::Diagnostics here because we may have no open document // Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position. // for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand. // When using them later in the diagnostics picker, we calculate them on-demand.
self.editor match self.editor.diagnostics.entry(params.uri) {
.diagnostics Entry::Occupied(o) => {
.insert(params.uri, params.diagnostics); let current_diagnostics = o.into_mut();
// there may entries of other language servers, which is why we can't overwrite the whole entry
current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
current_diagnostics.append(&mut diagnostics);
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
current_diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
}
Entry::Vacant(v) => {
diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
v.insert(diagnostics);
}
};
} }
Notification::ShowMessage(params) => { Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params); log::warn!("unhandled window/showMessage: {:?}", params);
@ -950,10 +969,8 @@ impl Application {
.editor .editor
.documents_mut() .documents_mut()
.filter_map(|doc| { .filter_map(|doc| {
if doc.language_server().map(|server| server.id()) if doc.supports_language_server(server_id) {
== Some(server_id) doc.clear_diagnostics(server_id);
{
doc.set_diagnostics(Vec::new());
doc.url() doc.url()
} else { } else {
None None
@ -1029,31 +1046,21 @@ impl Application {
})) }))
} }
Ok(MethodCall::WorkspaceFolders) => { Ok(MethodCall::WorkspaceFolders) => {
let language_server = Ok(json!(&*language_server!().workspace_folders().await))
self.editor.language_servers.get_by_id(server_id).unwrap();
Ok(json!(&*language_server.workspace_folders().await))
} }
Ok(MethodCall::WorkspaceConfiguration(params)) => { Ok(MethodCall::WorkspaceConfiguration(params)) => {
let language_server = language_server!();
let result: Vec<_> = params let result: Vec<_> = params
.items .items
.iter() .iter()
.map(|item| { .map(|item| {
let mut config = match &item.scope_uri { let mut config = language_server.config()?;
Some(scope) => {
let path = scope.to_file_path().ok()?;
let doc = self.editor.document_by_path(path)?;
doc.language_config()?.config.as_ref()?
}
None => self
.editor
.language_servers
.get_by_id(server_id)?
.config()?,
};
if let Some(section) = item.section.as_ref() { if let Some(section) = item.section.as_ref() {
for part in section.split('.') { // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
config = config.get(part)?; if !section.is_empty() {
for part in section.split('.') {
config = config.get(part)?;
}
} }
} }
Some(config) Some(config)
@ -1074,15 +1081,7 @@ impl Application {
} }
}; };
let language_server = match self.editor.language_servers.get_by_id(server_id) { tokio::spawn(language_server!().reply(id, reply));
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
tokio::spawn(language_server.reply(id, reply));
} }
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
} }

@ -23,6 +23,7 @@ use helix_core::{
regex::{self, Regex, RegexBuilder}, regex::{self, Regex, RegexBuilder},
search::{self, CharMatcher}, search::{self, CharMatcher},
selection, shellwords, surround, selection, shellwords, surround,
syntax::LanguageServerFeature,
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
textobject, textobject,
tree_sitter::Node, tree_sitter::Node,
@ -54,13 +55,13 @@ use crate::{
job::Callback, job::Callback,
keymap::ReverseKeymap, keymap::ReverseKeymap,
ui::{ ui::{
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker, self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
Popup, Prompt, PromptEvent, FilePicker, Picker, Popup, Prompt, PromptEvent,
}, },
}; };
use crate::job::{self, Jobs}; use crate::job::{self, Jobs};
use futures_util::StreamExt; use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize}; use std::{collections::HashSet, num::NonZeroUsize};
@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
fn goto_first_diag(cx: &mut Context) { fn goto_first_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().first() { let selection = match doc.shown_diagnostics().next() {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
fn goto_last_diag(cx: &mut Context) { fn goto_last_diag(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let selection = match doc.diagnostics().last() { let selection = match doc.shown_diagnostics().last() {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return, None => return,
}; };
@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diag = doc let diag = doc
.diagnostics() .shown_diagnostics()
.iter()
.find(|diag| diag.range.start > cursor_pos) .find(|diag| diag.range.start > cursor_pos)
.or_else(|| doc.diagnostics().first()); .or_else(|| doc.shown_diagnostics().next());
let selection = match diag { let selection = match diag {
Some(diag) => Selection::single(diag.range.start, diag.range.end), Some(diag) => Selection::single(diag.range.start, diag.range.end),
@ -3075,11 +3075,10 @@ fn goto_prev_diag(cx: &mut Context) {
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diag = doc let diag = doc
.diagnostics() .shown_diagnostics()
.iter()
.rev() .rev()
.find(|diag| diag.range.start < cursor_pos) .find(|diag| diag.range.start < cursor_pos)
.or_else(|| doc.diagnostics().last()); .or_else(|| doc.shown_diagnostics().last());
let selection = match diag { let selection = match diag {
// NOTE: the selection is reversed because we're jumping to the // NOTE: the selection is reversed because we're jumping to the
@ -3234,23 +3233,19 @@ pub mod insert {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches completion char, trigger completion // if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() { let trigger_completion = doc
Some(language_server) => language_server, .language_servers_with_feature(LanguageServerFeature::Completion)
None => return, .any(|ls| {
}; // TODO: what if trigger is multiple chars long
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
let capabilities = language_server.capabilities(); trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
});
if let Some(lsp::CompletionOptions { if trigger_completion {
trigger_characters: Some(triggers), cx.editor.clear_idle_timer();
.. super::completion(cx);
}) = &capabilities.completion_provider
{
// TODO: what if trigger is multiple chars long
if triggers.iter().any(|trigger| trigger.contains(ch)) {
cx.editor.clear_idle_timer();
super::completion(cx);
}
} }
} }
@ -3258,12 +3253,12 @@ pub mod insert {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches signature_help char, trigger // if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor); let doc = doc_mut!(cx.editor);
// The language_server!() macro is not used here since it will // TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
// print an "LSP not active for current buffer" message on let Some(language_server) = doc
// every keypress. .language_servers_with_feature(LanguageServerFeature::SignatureHelp)
let language_server = match doc.language_server() { .next()
Some(language_server) => language_server, else {
None => return, return;
}; };
let capabilities = language_server.capabilities(); let capabilities = language_server.capabilities();
@ -4046,55 +4041,60 @@ fn format_selections(cx: &mut Context) {
use helix_lsp::{lsp, util::range_to_lsp_range}; use helix_lsp::{lsp, util::range_to_lsp_range};
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id;
// via lsp if available // via lsp if available
// TODO: else via tree-sitter indentation calculations // TODO: else via tree-sitter indentation calculations
let language_server = match doc.language_server() { if doc.selection(view_id).len() != 1 {
Some(language_server) => language_server, cx.editor
None => return, .set_error("format_selections only supports a single selection for now");
return;
}
// TODO extra LanguageServerFeature::FormatSelections?
// maybe such that LanguageServerFeature::Format contains it as well
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::Format)
.find(|ls| {
matches!(
ls.capabilities().document_range_formatting_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
})
else {
cx.editor
.set_error("No configured language server does not support range formatting");
return;
}; };
let offset_encoding = language_server.offset_encoding();
let ranges: Vec<lsp::Range> = doc let ranges: Vec<lsp::Range> = doc
.selection(view.id) .selection(view_id)
.iter() .iter()
.map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding())) .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
.collect(); .collect();
if ranges.len() != 1 {
cx.editor
.set_error("format_selections only supports a single selection for now");
return;
}
// TODO: handle fails // TODO: handle fails
// TODO: concurrent map over all ranges // TODO: concurrent map over all ranges
let range = ranges[0]; let range = ranges[0];
let request = match language_server.text_document_range_formatting( let future = language_server
doc.identifier(), .text_document_range_formatting(
range, doc.identifier(),
lsp::FormattingOptions::default(), range,
None, lsp::FormattingOptions::default(),
) { None,
Some(future) => future, )
None => { .unwrap();
cx.editor
.set_error("Language server does not support range formatting");
return;
}
};
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default(); let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
let transaction = helix_lsp::util::generate_transaction_from_edits( let transaction =
doc.text(), helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
edits,
language_server.offset_encoding(),
);
doc.apply(&transaction, view.id); doc.apply(&transaction, view_id);
} }
fn join_selections_impl(cx: &mut Context, select_space: bool) { fn join_selections_impl(cx: &mut Context, select_space: bool) {
@ -4231,21 +4231,46 @@ pub fn completion(cx: &mut Context) {
doc.savepoint(view) doc.savepoint(view)
}; };
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let offset_encoding = language_server.offset_encoding();
let text = savepoint.text.clone(); let text = savepoint.text.clone();
let cursor = savepoint.cursor(); let cursor = savepoint.cursor();
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding); let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let language_server_id = language_server.id();
let offset_encoding = language_server.offset_encoding();
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
let doc_id = doc.identifier();
let completion_request = language_server.completion(doc_id, pos, None).unwrap();
async move {
let json = completion_request.await?;
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => Vec::new(),
}
.into_iter()
.map(|item| CompletionItem {
item,
language_server_id,
resolved: false,
})
.collect();
let future = match language_server.completion(doc.identifier(), pos, None) { anyhow::Ok(items)
Some(future) => future, }
None => return, })
}; .collect();
// setup a channel that allows the request to be canceled // setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -4254,12 +4279,20 @@ pub fn completion(cx: &mut Context) {
// and the associated request is automatically dropped // and the associated request is automatically dropped
cx.editor.completion_request_handle = Some(tx); cx.editor.completion_request_handle = Some(tx);
let future = async move { let future = async move {
let items_future = async move {
let mut items = Vec::new();
// TODO if one completion request errors, all other completion requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
items.append(&mut lsp_items);
}
anyhow::Ok(items)
};
tokio::select! { tokio::select! {
biased; biased;
_ = rx => { _ = rx => {
Ok(serde_json::Value::Null) Ok(Vec::new())
} }
res = future => { res = items_future => {
res res
} }
} }
@ -4293,9 +4326,9 @@ pub fn completion(cx: &mut Context) {
}, },
)); ));
cx.callback( cx.jobs.callback(async move {
future, let items = future.await?;
move |editor, compositor, response: Option<lsp::CompletionResponse>| { let call = move |editor: &mut Editor, compositor: &mut Compositor| {
let (view, doc) = current_ref!(editor); let (view, doc) = current_ref!(editor);
// check if the completion request is stale. // check if the completion request is stale.
// //
@ -4306,16 +4339,6 @@ pub fn completion(cx: &mut Context) {
return; return;
} }
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => Vec::new(),
};
if items.is_empty() { if items.is_empty() {
// editor.set_error("No completion available"); // editor.set_error("No completion available");
return; return;
@ -4326,7 +4349,6 @@ pub fn completion(cx: &mut Context) {
editor, editor,
savepoint, savepoint,
items, items,
offset_encoding,
start_offset, start_offset,
trigger_offset, trigger_offset,
size, size,
@ -4340,8 +4362,9 @@ pub fn completion(cx: &mut Context) {
{ {
compositor.remove(SignatureHelp::ID); compositor.remove(SignatureHelp::ID);
} }
}, };
); Ok(Callback::EditorCompositor(Box::new(call)))
});
} }
// comments // comments
@ -5141,7 +5164,7 @@ async fn shell_impl_async(
helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input) helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
.await?; .await?;
} }
Ok::<_, anyhow::Error>(()) anyhow::Ok(())
}); });
let (output, _) = tokio::join! { let (output, _) = tokio::join! {
process.wait_with_output(), process.wait_with_output(),

File diff suppressed because it is too large Load Diff

@ -1329,26 +1329,22 @@ fn lsp_workspace_command(
if event != PromptEvent::Validate { if event != PromptEvent::Validate {
return Ok(()); return Ok(());
} }
let doc = doc!(cx.editor);
let (_, doc) = current!(cx.editor); let Some((language_server_id, options)) = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
let language_server = match doc.language_server() { .find_map(|ls| {
Some(language_server) => language_server, ls.capabilities()
None => { .execute_command_provider
cx.editor .as_ref()
.set_status("Language server not active for current buffer"); .map(|options| (ls.id(), options))
return Ok(()); })
} else {
cx.editor.set_status(
"No active language servers for this document support workspace commands",
);
return Ok(());
}; };
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
cx.editor
.set_status("Workspace commands are not supported for this language server");
return Ok(());
}
};
if args.is_empty() { if args.is_empty() {
let commands = options let commands = options
.commands .commands
@ -1362,8 +1358,8 @@ fn lsp_workspace_command(
let callback = async move { let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new( let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| { move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| { let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone()); execute_lsp_command(cx.editor, language_server_id, command.clone());
}); });
compositor.push(Box::new(overlaid(picker))) compositor.push(Box::new(overlaid(picker)))
}, },
@ -1376,6 +1372,7 @@ fn lsp_workspace_command(
if options.commands.iter().any(|c| c == &command) { if options.commands.iter().any(|c| c == &command) {
execute_lsp_command( execute_lsp_command(
cx.editor, cx.editor,
language_server_id,
helix_lsp::lsp::Command { helix_lsp::lsp::Command {
title: command.clone(), title: command.clone(),
arguments: None, arguments: None,
@ -1407,7 +1404,6 @@ fn lsp_restart(
.language_config() .language_config()
.context("LSP not defined for the current document")?; .context("LSP not defined for the current document")?;
let scope = config.scope.clone();
cx.editor.language_servers.restart( cx.editor.language_servers.restart(
config, config,
doc.path(), doc.path(),
@ -1420,13 +1416,22 @@ fn lsp_restart(
.editor .editor
.documents() .documents()
.filter_map(|doc| match doc.language_config() { .filter_map(|doc| match doc.language_config() {
Some(config) if config.scope.eq(&scope) => Some(doc.id()), Some(config)
if config.language_servers.iter().any(|ls| {
config
.language_servers
.iter()
.any(|restarted_ls| restarted_ls.name == ls.name)
}) =>
{
Some(doc.id())
}
_ => None, _ => None,
}) })
.collect(); .collect();
for document_id in document_ids_to_refresh { for document_id in document_ids_to_refresh {
cx.editor.refresh_language_server(document_id); cx.editor.refresh_language_servers(document_id);
} }
Ok(()) Ok(())
@ -1441,22 +1446,18 @@ fn lsp_stop(
return Ok(()); return Ok(());
} }
let doc = doc!(cx.editor); let ls_shutdown_names = doc!(cx.editor)
.language_servers()
.map(|ls| ls.name().to_string())
.collect::<Vec<_>>();
let ls_id = doc for ls_name in &ls_shutdown_names {
.language_server() cx.editor.language_servers.stop(ls_name);
.map(|ls| ls.id())
.context("LSP not running for the current document")?;
let config = doc for doc in cx.editor.documents_mut() {
.language_config() if let Some(client) = doc.remove_language_server_by_name(ls_name) {
.context("LSP not defined for the current document")?; doc.clear_diagnostics(client.id());
cx.editor.language_servers.stop(config); }
for doc in cx.editor.documents_mut() {
if doc.language_server().map_or(false, |ls| ls.id() == ls_id) {
doc.set_language_server(None);
doc.set_diagnostics(Default::default());
} }
} }
@ -1850,7 +1851,7 @@ fn language(
doc.detect_indent_and_line_ending(); doc.detect_indent_and_line_ending();
let id = doc.id(); let id = doc.id();
cx.editor.refresh_language_server(id); cx.editor.refresh_language_servers(id);
Ok(()) Ok(())
} }
@ -2588,14 +2589,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand { TypableCommand {
name: "lsp-restart", name: "lsp-restart",
aliases: &[], aliases: &[],
doc: "Restarts the Language Server that is in use by the current doc", doc: "Restarts the language servers used by the current doc",
fun: lsp_restart, fun: lsp_restart,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },
TypableCommand { TypableCommand {
name: "lsp-stop", name: "lsp-stop",
aliases: &[], aliases: &[],
doc: "Stops the Language Server that is in use by the current doc", doc: "Stops the language servers that are used by the current doc",
fun: lsp_stop, fun: lsp_stop,
signature: CommandSignature::none(), signature: CommandSignature::none(),
}, },

@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> {
for lang in &syn_loader_conf.language { for lang in &syn_loader_conf.language {
column(&lang.language_id, Color::Reset); column(&lang.language_id, Color::Reset);
let lsp = lang // TODO multiple language servers (check binary for each supported language server, not just the first)
.language_server
.as_ref() let lsp = lang.language_servers.first().and_then(|ls| {
.map(|lsp| lsp.command.to_string()); syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| config.command.clone())
});
check_binary(lsp); check_binary(lsp);
let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@ -264,11 +268,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
} }
}; };
// TODO multiple language servers
probe_protocol( probe_protocol(
"language server", "language server",
lang.language_server lang.language_servers.first().and_then(|ls| {
.as_ref() syn_loader_conf
.map(|lsp| lsp.command.to_string()), .language_server
.get(&ls.name)
.map(|config| config.command.clone())
}),
)?; )?;
probe_protocol( probe_protocol(

@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
use crate::commands; use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::{lsp, util}; use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem { impl menu::Item for CompletionItem {
type Data = (); type Data = ();
@ -38,6 +38,7 @@ impl menu::Item for CompletionItem {
|| self.item.tags.as_ref().map_or(false, |tags| { || self.item.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED) tags.contains(&lsp::CompletionItemTag::DEPRECATED)
}); });
menu::Row::new(vec![ menu::Row::new(vec![
menu::Cell::from(Span::styled( menu::Cell::from(Span::styled(
self.item.label.as_str(), self.item.label.as_str(),
@ -79,19 +80,15 @@ impl menu::Item for CompletionItem {
} }
None => "", None => "",
}), }),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
]) ])
} }
} }
#[derive(Debug, PartialEq, Default, Clone)] #[derive(Debug, PartialEq, Default, Clone)]
struct CompletionItem { pub struct CompletionItem {
item: lsp::CompletionItem, pub item: lsp::CompletionItem,
resolved: bool, pub language_server_id: usize,
pub resolved: bool,
} }
/// Wraps a Menu. /// Wraps a Menu.
@ -109,29 +106,21 @@ impl Completion {
pub fn new( pub fn new(
editor: &Editor, editor: &Editor,
savepoint: Arc<SavePoint>, savepoint: Arc<SavePoint>,
mut items: Vec<lsp::CompletionItem>, mut items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
let replace_mode = editor.config().completion_replace; let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server) // Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false)); items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
let items = items
.into_iter()
.map(|item| CompletionItem {
item,
resolved: false,
})
.collect();
// Then create the menu // Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn item_to_transaction(
doc: &Document, doc: &Document,
view_id: ViewId, view_id: ViewId,
item: &CompletionItem, item: &lsp::CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: OffsetEncoding,
trigger_offset: usize, trigger_offset: usize,
include_placeholder: bool, include_placeholder: bool,
replace_mode: bool, replace_mode: bool,
@ -141,7 +130,7 @@ impl Completion {
let text = doc.text().slice(..); let text = doc.text().slice(..);
let primary_cursor = selection.primary().cursor(text); let primary_cursor = selection.primary().cursor(text);
let (edit_offset, new_text) = if let Some(edit) = &item.item.text_edit { let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
let edit = match edit { let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => { lsp::CompletionTextEdit::InsertAndReplace(item) => {
@ -164,10 +153,9 @@ impl Completion {
(Some((start_offset, end_offset)), edit.new_text) (Some((start_offset, end_offset)), edit.new_text)
} else { } else {
let new_text = item let new_text = item
.item
.insert_text .insert_text
.clone() .clone()
.unwrap_or_else(|| item.item.label.clone()); .unwrap_or_else(|| item.label.clone());
// check that we are still at the correct savepoint // check that we are still at the correct savepoint
// we can still generate a transaction regardless but if the // we can still generate a transaction regardless but if the
// document changed (and not just the selection) then we will // document changed (and not just the selection) then we will
@ -176,9 +164,9 @@ impl Completion {
(None, new_text) (None, new_text)
}; };
if matches!(item.item.kind, Some(lsp::CompletionItemKind::SNIPPET)) if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|| matches!( || matches!(
item.item.insert_text_format, item.insert_text_format,
Some(lsp::InsertTextFormat::SNIPPET) Some(lsp::InsertTextFormat::SNIPPET)
) )
{ {
@ -223,6 +211,23 @@ impl Completion {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
macro_rules! language_server {
($item:expr) => {
match editor
.language_servers
.get_by_id($item.language_server_id)
{
Some(ls) => ls,
None => {
editor.set_error("completions are outdated");
// TODO close the completion menu somehow,
// currently there is no trivial way to access the EditorView to close the completion menu
return;
}
}
};
}
match event { match event {
PromptEvent::Abort => {} PromptEvent::Abort => {}
PromptEvent::Update => { PromptEvent::Update => {
@ -250,8 +255,8 @@ impl Completion {
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
item, &item.item,
offset_encoding, language_server!(item).offset_encoding(),
trigger_offset, trigger_offset,
true, true,
replace_mode, replace_mode,
@ -267,10 +272,18 @@ impl Completion {
// always present here // always present here
let mut item = item.unwrap().clone(); let mut item = item.unwrap().clone();
let language_server = language_server!(item);
let offset_encoding = language_server.offset_encoding();
let language_server = editor
.language_servers
.get_by_id(item.language_server_id)
.unwrap();
// resolve item if not yet resolved // resolve item if not yet resolved
if !item.resolved { if !item.resolved {
if let Some(resolved) = if let Some(resolved) =
Self::resolve_completion_item(doc, item.item.clone()) Self::resolve_completion_item(language_server, item.item.clone())
{ {
item.item = resolved; item.item = resolved;
} }
@ -280,7 +293,7 @@ impl Completion {
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
&item, &item.item,
offset_encoding, offset_encoding,
trigger_offset, trigger_offset,
false, false,
@ -323,11 +336,9 @@ impl Completion {
} }
fn resolve_completion_item( fn resolve_completion_item(
doc: &Document, language_server: &helix_lsp::Client,
completion_item: lsp::CompletionItem, completion_item: lsp::CompletionItem,
) -> Option<lsp::CompletionItem> { ) -> Option<lsp::CompletionItem> {
let language_server = doc.language_server()?;
let future = language_server.resolve_completion_item(completion_item)?; let future = language_server.resolve_completion_item(completion_item)?;
let response = helix_lsp::block_on(future); let response = helix_lsp::block_on(future);
match response { match response {
@ -398,16 +409,10 @@ impl Completion {
_ => return false, _ => return false,
}; };
let language_server = match doc!(cx.editor).language_server() { let Some(language_server) = cx.editor.language_server_by_id(current_item.language_server_id) else { return false; };
Some(language_server) => language_server,
None => return false,
};
// This method should not block the compositor so we handle the response asynchronously. // This method should not block the compositor so we handle the response asynchronously.
let future = match language_server.resolve_completion_item(current_item.item.clone()) { let Some(future) = language_server.resolve_completion_item(current_item.item.clone()) else { return false; };
Some(future) => future,
None => return false,
};
cx.callback( cx.callback(
future, future,
@ -422,13 +427,13 @@ impl Completion {
.unwrap() .unwrap()
.completion .completion
{ {
completion.replace_item( let resolved_item = CompletionItem {
current_item, item: resolved_item,
CompletionItem { language_server_id: current_item.language_server_id,
item: resolved_item, resolved: true,
resolved: true, };
},
); completion.replace_item(current_item, resolved_item);
} }
}, },
); );

@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span}; use tui::{buffer::Buffer as Surface, text::Span};
use super::statusline; use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp}; use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView { pub struct EditorView {
@ -650,7 +650,7 @@ impl EditorView {
.primary() .primary()
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| { let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
}); });
@ -953,20 +953,13 @@ impl EditorView {
&mut self, &mut self,
editor: &mut Editor, editor: &mut Editor,
savepoint: Arc<SavePoint>, savepoint: Arc<SavePoint>,
items: Vec<helix_lsp::lsp::CompletionItem>, items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
size: Rect, size: Rect,
) -> Option<Rect> { ) -> Option<Rect> {
let mut completion = Completion::new( let mut completion =
editor, Completion::new(editor, savepoint, items, start_offset, trigger_offset);
savepoint,
items,
offset_encoding,
start_offset,
trigger_offset,
);
if completion.is_empty() { if completion.is_empty() {
// skip if we got no completion results // skip if we got no completion results

@ -17,7 +17,7 @@ mod text;
use crate::compositor::{Component, Compositor}; use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry; use crate::filter_picker_entry;
use crate::job::{self, Callback}; use crate::job::{self, Callback};
pub use completion::Completion; pub use completion::{Completion, CompletionItem};
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
@ -238,6 +238,7 @@ pub mod completers {
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme; use helix_view::theme;
use helix_view::{editor::Config, Editor}; use helix_view::{editor::Config, Editor};
@ -393,20 +394,11 @@ pub mod completers {
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default(); let matcher = Matcher::default();
let (_, doc) = current_ref!(editor); let Some(options) = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
let language_server = match doc.language_server() { .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
Some(language_server) => language_server, else {
None => { return vec![];
return vec![];
}
};
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
return vec![];
}
}; };
let mut matches: Vec<_> = options let mut matches: Vec<_> = options

@ -197,15 +197,15 @@ where
); );
} }
// TODO think about handling multiple language servers
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F) fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy, F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{ {
let language_server = context.doc.language_servers().next();
write( write(
context, context,
context language_server
.doc
.language_server()
.and_then(|srv| { .and_then(|srv| {
context context
.spinners .spinners
@ -225,8 +225,7 @@ where
{ {
let (warnings, errors) = context let (warnings, errors) = context
.doc .doc
.diagnostics() .shown_diagnostics()
.iter()
.fold((0, 0), |mut counts, diag| { .fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
match diag.severity { match diag.severity {
@ -266,7 +265,7 @@ where
.diagnostics .diagnostics
.values() .values()
.flatten() .flatten()
.fold((0, 0), |mut counts, diag| { .fold((0, 0), |mut counts, (diag, _)| {
match diag.severity { match diag.severity {
Some(DiagnosticSeverity::WARNING) => counts.0 += 1, Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,

@ -6,7 +6,7 @@ use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs; use helix_core::auto_pairs::AutoPairs;
use helix_core::doc_formatter::TextFormat; use helix_core::doc_formatter::TextFormat;
use helix_core::encoding::Encoding; use helix_core::encoding::Encoding;
use helix_core::syntax::Highlight; use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_core::Range; use helix_core::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry}; use helix_vcs::{DiffHandle, DiffProviderRegistry};
@ -179,8 +179,8 @@ pub struct Document {
version: i32, // should be usize? version: i32, // should be usize?
pub(crate) modified_since_accessed: bool, pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>, pub(crate) diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>, pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
diff_handle: Option<DiffHandle>, diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@ -580,7 +580,7 @@ where
*mut_ref = f(mem::take(mut_ref)); *mut_ref = f(mem::take(mut_ref));
} }
use helix_lsp::lsp; use helix_lsp::{lsp, Client, LanguageServerName};
use url::Url; use url::Url;
impl Document { impl Document {
@ -616,7 +616,7 @@ impl Document {
last_saved_time: SystemTime::now(), last_saved_time: SystemTime::now(),
last_saved_revision: 0, last_saved_revision: 0,
modified_since_accessed: false, modified_since_accessed: false,
language_server: None, language_servers: HashMap::new(),
diff_handle: None, diff_handle: None,
config, config,
version_control_head: None, version_control_head: None,
@ -730,10 +730,12 @@ impl Document {
return Some(formatting_future.boxed()); return Some(formatting_future.boxed());
}; };
let language_server = self.language_server()?;
let text = self.text.clone(); let text = self.text.clone();
// finds first language server that supports formatting and then formats
let language_server = self
.language_servers_with_feature(LanguageServerFeature::Format)
.next()?;
let offset_encoding = language_server.offset_encoding(); let offset_encoding = language_server.offset_encoding();
let request = language_server.text_document_formatting( let request = language_server.text_document_formatting(
self.identifier(), self.identifier(),
lsp::FormattingOptions { lsp::FormattingOptions {
@ -797,13 +799,12 @@ impl Document {
if self.path.is_none() { if self.path.is_none() {
bail!("Can't save with no path set!"); bail!("Can't save with no path set!");
} }
self.path.as_ref().unwrap().clone() self.path.as_ref().unwrap().clone()
} }
}; };
let identifier = self.path().map(|_| self.identifier()); let identifier = self.path().map(|_| self.identifier());
let language_server = self.language_server.clone(); let language_servers = self.language_servers.clone();
// mark changes up to now as saved // mark changes up to now as saved
let current_rev = self.get_current_revision(); let current_rev = self.get_current_revision();
@ -847,14 +848,13 @@ impl Document {
text: text.clone(), text: text.clone(),
}; };
if let Some(language_server) = language_server { for (_, language_server) in language_servers {
if !language_server.is_initialized() { if !language_server.is_initialized() {
return Ok(event); return Ok(event);
} }
if let Some(identifier) = &identifier {
if let Some(identifier) = identifier {
if let Some(notification) = if let Some(notification) =
language_server.text_document_did_save(identifier, &text) language_server.text_document_did_save(identifier.clone(), &text)
{ {
notification.await?; notification.await?;
} }
@ -1004,11 +1004,6 @@ impl Document {
Ok(()) Ok(())
} }
/// Set the LSP.
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
self.language_server = language_server;
}
/// Select text within the [`Document`]. /// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
// TODO: use a transaction? // TODO: use a transaction?
@ -1159,7 +1154,7 @@ impl Document {
if emit_lsp_notification { if emit_lsp_notification {
// emit lsp notification // emit lsp notification
if let Some(language_server) = self.language_server() { for language_server in self.language_servers() {
let notify = language_server.text_document_did_change( let notify = language_server.text_document_did_change(
self.versioned_identifier(), self.versioned_identifier(),
&old_doc, &old_doc,
@ -1415,18 +1410,13 @@ impl Document {
.map(|language| language.language_id.as_str()) .map(|language| language.language_id.as_str())
} }
/// Language ID for the document. Either the `language-id` from the /// Language ID for the document. Either the `language-id`,
/// `language-server` configuration, or the document language if no /// or the document language name if no `language-id` has been specified.
/// `language-id` has been specified.
pub fn language_id(&self) -> Option<&str> { pub fn language_id(&self) -> Option<&str> {
let language_config = self.language.as_deref()?; self.language_config()?
.language_server_language_id
language_config
.language_server
.as_ref()?
.language_id
.as_deref() .as_deref()
.or(Some(language_config.language_id.as_str())) .or_else(|| self.language_name())
} }
/// Corresponding [`LanguageConfiguration`]. /// Corresponding [`LanguageConfiguration`].
@ -1439,10 +1429,45 @@ impl Document {
self.version self.version
} }
/// Language server if it has been initialized. /// maintains the order as configured in the language_servers TOML array
pub fn language_server(&self) -> Option<&helix_lsp::Client> { pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
let server = self.language_server.as_deref()?; self.language_config().into_iter().flat_map(move |config| {
server.is_initialized().then_some(server) config.language_servers.iter().filter_map(move |features| {
let ls = &**self.language_servers.get(&features.name)?;
if ls.is_initialized() {
Some(ls)
} else {
None
}
})
})
}
pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> {
self.language_servers.remove(name)
}
pub fn language_servers_with_feature(
&self,
feature: LanguageServerFeature,
) -> impl Iterator<Item = &helix_lsp::Client> {
self.language_config().into_iter().flat_map(move |config| {
config.language_servers.iter().filter_map(move |features| {
let ls = &**self.language_servers.get(&features.name)?;
if ls.is_initialized()
&& ls.supports_feature(feature)
&& features.has_feature(feature)
{
Some(ls)
} else {
None
}
})
})
}
pub fn supports_language_server(&self, id: usize) -> bool {
self.language_servers().any(|l| l.id() == id)
} }
pub fn diff_handle(&self) -> Option<&DiffHandle> { pub fn diff_handle(&self) -> Option<&DiffHandle> {
@ -1565,12 +1590,29 @@ impl Document {
&self.diagnostics &self.diagnostics
} }
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator {
self.diagnostics = diagnostics; self.diagnostics.iter().filter(|d| {
self.language_servers_with_feature(LanguageServerFeature::Diagnostics)
.any(|ls| ls.id() == d.language_server_id)
})
}
pub fn replace_diagnostics(
&mut self,
mut diagnostics: Vec<Diagnostic>,
language_server_id: usize,
) {
self.clear_diagnostics(language_server_id);
self.diagnostics.append(&mut diagnostics);
self.diagnostics self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range); .sort_unstable_by_key(|diagnostic| diagnostic.range);
} }
pub fn clear_diagnostics(&mut self, language_server_id: usize) {
self.diagnostics
.retain(|d| d.language_server_id != language_server_id);
}
/// Get the document's auto pairs. If the document has a recognized /// Get the document's auto pairs. If the document has a recognized
/// language config with auto pairs configured, returns that; /// language config with auto pairs configured, returns that;
/// otherwise, falls back to the global auto pairs config. If the global /// otherwise, falls back to the global auto pairs config. If the global

@ -818,7 +818,7 @@ pub struct Editor {
pub macro_recording: Option<(char, Vec<KeyEvent>)>, pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>, pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
pub diff_providers: DiffProviderRegistry, pub diff_providers: DiffProviderRegistry,
pub debugger: Option<dap::Client>, pub debugger: Option<dap::Client>,
@ -874,7 +874,7 @@ pub struct Editor {
/// times during rendering and should not be set by other functions. /// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>, pub cursor_cache: Cell<Option<Option<Position>>>,
/// When a new completion request is sent to the server old /// When a new completion request is sent to the server old
/// unifinished request must be dropped. Each completion /// unfinished request must be dropped. Each completion
/// request is associated with a channel that cancels /// request is associated with a channel that cancels
/// when the channel is dropped. That channel is stored /// when the channel is dropped. That channel is stored
/// here. When a new completion request is sent this /// here. When a new completion request is sent this
@ -941,6 +941,7 @@ impl Editor {
syn_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>, config: Arc<dyn DynAccess<Config>>,
) -> Self { ) -> Self {
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
let conf = config.load(); let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into(); let auto_pairs = (&conf.auto_pairs).into();
@ -960,7 +961,7 @@ impl Editor {
macro_recording: None, macro_recording: None,
macro_replaying: Vec::new(), macro_replaying: Vec::new(),
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(), language_servers,
diagnostics: BTreeMap::new(), diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(), diff_providers: DiffProviderRegistry::default(),
debugger: None, debugger: None,
@ -1092,60 +1093,75 @@ impl Editor {
self._refresh(); self._refresh();
} }
#[inline]
pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
self.language_servers.get_by_id(language_server_id)
}
/// Refreshes the language server for a given document /// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_server(doc_id) self.launch_language_servers(doc_id)
} }
/// Launch a language server for a given document /// Launch a language server for a given document
fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> { fn launch_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
if !self.config().lsp.enable { if !self.config().lsp.enable {
return None; return None;
} }
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it
let doc = self.document(doc_id)?; let doc = self.documents.get_mut(&doc_id)?;
let doc_url = doc.url()?;
let (lang, path) = (doc.language.clone(), doc.path().cloned()); let (lang, path) = (doc.language.clone(), doc.path().cloned());
let config = doc.config.load(); let config = doc.config.load();
let root_dirs = &config.workspace_lsp_roots; let root_dirs = &config.workspace_lsp_roots;
// try to find a language server based on the language name // try to find language servers based on the language name
let language_server = lang.as_ref().and_then(|language| { let language_servers = lang.as_ref().and_then(|language| {
self.language_servers self.language_servers
.get(language, path.as_ref(), root_dirs, config.lsp.snippets) .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}", "Failed to initialize the language servers for `{}` {{ {} }}",
language.scope(), language.scope(),
e e
) )
}) })
.ok() .ok()
.flatten()
}); });
let doc = self.document_mut(doc_id)?; if let Some(language_servers) = language_servers {
let doc_url = doc.url()?; let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
if let Some(language_server) = language_server { // only spawn new language servers if the servers aren't the same
// only spawn a new lang server if the servers aren't the same
if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); let doc_language_servers_not_in_registry =
doc.language_servers.iter().filter(|(name, doc_ls)| {
language_servers
.get(*name)
.map_or(true, |ls| ls.id() != doc_ls.id())
});
for (_, language_server) in doc_language_servers_not_in_registry {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
doc.language_servers
.get(*name)
.map_or(true, |doc_ls| ls.id() != doc_ls.id())
});
for (_, language_server) in language_servers_not_in_doc {
// TODO: this now races with on_init code if the init happens too quickly // TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open( tokio::spawn(language_server.text_document_did_open(
doc_url, doc_url.clone(),
doc.version(), doc.version(),
doc.text(), doc.text(),
language_id, language_id.clone(),
)); ));
doc.set_language_server(Some(language_server));
} }
doc.language_servers = language_servers;
} }
Some(()) Some(())
} }
@ -1338,7 +1354,7 @@ impl Editor {
doc.set_version_control_head(self.diff_providers.get_current_head_name(&path)); doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
let id = self.new_document(doc); let id = self.new_document(doc);
let _ = self.launch_language_server(id); let _ = self.launch_language_servers(id);
id id
}; };
@ -1368,7 +1384,7 @@ impl Editor {
// This will also disallow any follow-up writes // This will also disallow any follow-up writes
self.saves.remove(&doc_id); self.saves.remove(&doc_id);
if let Some(language_server) = doc.language_server() { for language_server in doc.language_servers() {
// TODO: track error // TODO: track error
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }

@ -1,5 +1,7 @@
use std::fmt::Write; use std::fmt::Write;
use helix_core::syntax::LanguageServerFeature;
use crate::{ use crate::{
editor::GutterType, editor::GutterType,
graphics::{Style, UnderlineStyle}, graphics::{Style, UnderlineStyle},
@ -55,7 +57,7 @@ pub fn diagnostic<'doc>(
let error = theme.get("error"); let error = theme.get("error");
let info = theme.get("info"); let info = theme.get("info");
let hint = theme.get("hint"); let hint = theme.get("hint");
let diagnostics = doc.diagnostics(); let diagnostics = &doc.diagnostics;
Box::new( Box::new(
move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
@ -63,28 +65,24 @@ pub fn diagnostic<'doc>(
return None; return None;
} }
use helix_core::diagnostic::Severity; use helix_core::diagnostic::Severity;
if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line);
let after = diagnostics[index..].iter().take_while(|d| d.line == line); let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..]
.iter()
let before = diagnostics[..index] .take_while(|d| {
.iter() d.line == line
.rev() && doc
.take_while(|d| d.line == line); .language_servers_with_feature(LanguageServerFeature::Diagnostics)
.any(|ls| ls.id() == d.language_server_id)
let diagnostics_on_line = after.chain(before); });
diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
// This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search. write!(out, "●").ok();
let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap(); match d.severity {
write!(out, "●").unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error, Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning, Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info, Some(Severity::Info) => info,
Some(Severity::Hint) => hint, Some(Severity::Hint) => hint,
}); }
} })
None
}, },
) )
} }

File diff suppressed because it is too large Load Diff

@ -96,11 +96,12 @@ pub fn lang_features() -> Result<String, DynError> {
); );
} }
row.push( row.push(
lc.language_server lc.language_servers
.as_ref() .iter()
.map(|s| s.command.clone()) .filter_map(|ls| config.language_server.get(&ls.name))
.map(|c| md_mono(&c)) .map(|s| md_mono(&s.command.clone()))
.unwrap_or_default(), .collect::<Vec<_>>()
.join(", "),
); );
md.push_str(&md_table_row(&row)); md.push_str(&md_table_row(&row));

Loading…
Cancel
Save