1
0
Fork 0
mirror of https://github.com/helix-editor/helix synced 2024-05-07 11:36:04 +02:00

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

Add support for multiple language servers per language
This commit is contained in:
Blaž Hrastnik 2023-05-19 09:39:35 +09:00 committed by GitHub
commit 53f47bc477
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1632 additions and 1137 deletions

View File

@ -50,8 +50,8 @@
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
| `:lsp-restart` | Restarts the language servers used by the current doc |
| `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |

View File

@ -9,6 +9,7 @@ ## Language configuration
necessary configuration for the new language. For more information on
language configuration, refer to the
[language configuration section](../languages.md) of the documentation.
A new language server can be added by extending the `[language-server]` table in the same file.
2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation.

View File

@ -18,6 +18,9 @@ ## `languages.toml` files
```toml
# in <config_dir>/helix/languages.toml
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]]
name = "rust"
auto-format = false
@ -41,8 +44,8 @@ ## Language configuration
file-types = ["mylang", "myl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
```
These configuration keys are available:
@ -50,6 +53,7 @@ ## Language configuration
| Key | Description |
| ---- | ----------- |
| `name` | The name of the language |
| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@ -59,7 +63,7 @@ ## Language configuration
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| `comment-token` | The token to use as a comment-token |
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
| `language-server` | The Language Server to run. See the Language Server configuration section below. |
| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
| `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
@ -92,31 +96,102 @@ ### File-type detection and the `file-types` key
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.
### Language Server configuration
## Language Server configuration
The `language-server` field takes the following keys:
Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
| Key | Description |
| --- | ----------- |
| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
For example:
The top-level `config` field is used to configure the LSP initialization options. A `format`
sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook).
```toml
[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
environment = { "ENV1" = "value1", "ENV2" = "value2" }
[language-server.efm-lsp-prettier]
command = "efm-langserver"
[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
These are the available options for a language server.
| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook).
For example with typescript:
```toml
[language-server.typescript-language-server]
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
```
### Configuring Language Servers for a language
The `language-servers` attribute in a language tells helix which language servers are used for this language.
They have to be defined in the `[language-server]` table as described in the previous section.
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers.
For example `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default)
The language configuration for typescript could look like this:
```toml
[[language]]
name = "typescript"
auto-format = true
# 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 } }
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
The source for a language's tree-sitter grammar is specified in a `[[grammar]]`

View File

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

View File

@ -16,8 +16,8 @@
use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, VecDeque},
fmt,
collections::{HashMap, HashSet, VecDeque},
fmt::{self, Display},
hash::{Hash, Hasher},
mem::{replace, transmute},
path::{Path, PathBuf},
@ -26,7 +26,7 @@
};
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};
@ -60,8 +60,11 @@ fn default_timeout() -> u64 {
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
}
impl Default for Configuration {
@ -75,7 +78,10 @@ fn default() -> Self {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String, // c-sharp, rust
pub language_id: String, // c-sharp, rust, tsx
#[serde(rename = "language-id")]
// see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
pub scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)]
@ -85,9 +91,6 @@ pub struct LanguageConfiguration {
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default)]
pub auto_format: bool,
@ -107,8 +110,13 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
#[serde(skip_serializing_if = "Option::is_none")]
pub language_server: Option<LanguageServerConfiguration>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_lang_features",
deserialize_with = "deserialize_lang_features"
)]
pub language_servers: Vec<LanguageServerFeatures>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>,
@ -208,6 +216,133 @@ fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
}
}
#[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)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
@ -217,9 +352,10 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")]
pub timeout: u64,
pub language_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -594,6 +730,8 @@ pub struct Loader {
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
scopes: ArcSwap<Vec<String>>,
}
@ -601,6 +739,7 @@ impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
@ -725,6 +864,10 @@ pub fn language_configs(&self) -> impl Iterator<Item = &Arc<LanguageConfiguratio
self.language_configs.iter()
}
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
&self.language_server_configs
}
pub fn set_scopes(&self, scopes: Vec<String>) {
self.scopes.store(Arc::new(scopes));
@ -2370,7 +2513,10 @@ fn test_textobject_queries() {
"#,
);
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
@ -2429,7 +2575,10 @@ fn test_parser() {
.map(String::from)
.collect();
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
@ -2532,7 +2681,10 @@ fn assert_pretty_print(
) {
let source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();

View File

@ -4,7 +4,7 @@
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 lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
@ -44,6 +44,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)]
pub struct Client {
id: usize,
name: String,
_process: Child,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
@ -166,8 +167,7 @@ fn add_workspace_folder(
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
}
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn start(
cmd: &str,
args: &[String],
@ -176,6 +176,7 @@ pub fn start(
root_markers: &[String],
manual_roots: &[PathBuf],
id: usize,
name: String,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@ -200,7 +201,7 @@ pub fn start(
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
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 = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
@ -225,6 +226,7 @@ pub fn start(
let client = Self {
id,
name,
_process: process,
server_tx,
request_counter: AtomicU64::new(0),
@ -240,6 +242,10 @@ pub fn start(
Ok((client, server_rx, initialize_notify))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> usize {
self.id
}
@ -270,6 +276,87 @@ pub fn capabilities(&self) -> &lsp::ServerCapabilities {
.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 {
self.capabilities()
.position_encoding
@ -1295,21 +1382,13 @@ pub fn code_actions(
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(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
if !self.supports_rename() {
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None;
}

View File

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

View File

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

View File

@ -30,6 +30,7 @@
use log::{debug, error, warn};
use std::{
collections::btree_map::Entry,
io::{stdin, stdout},
path::Path,
sync::Arc,
@ -564,7 +565,7 @@ pub fn handle_document_write(&mut self, doc_save_event: DocumentSavedEventResult
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
let _ = self.editor.refresh_language_server(id);
self.editor.refresh_language_servers(id);
}
// TODO: fix being overwritten by lsp
@ -662,6 +663,18 @@ pub async fn handle_language_server_message(
) {
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 {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
@ -677,14 +690,7 @@ pub async fn handle_language_server_message(
match notification {
Notification::Initialized => {
let 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;
}
};
let language_server = language_server!();
// Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's
@ -693,9 +699,10 @@ pub async fn handle_language_server_message(
tokio::spawn(language_server.did_change_configuration(config.clone()));
}
let docs = self.editor.documents().filter(|doc| {
doc.language_server().map(|server| server.id()) == Some(server_id)
});
let docs = self
.editor
.documents()
.filter(|doc| doc.supports_language_server(server_id));
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
@ -715,7 +722,7 @@ pub async fn handle_language_server_message(
));
}
}
Notification::PublishDiagnostics(mut params) => {
Notification::PublishDiagnostics(params) => {
let path = match params.uri.to_file_path() {
Ok(path) => path,
Err(_) => {
@ -723,6 +730,7 @@ pub async fn handle_language_server_message(
return;
}
};
let offset_encoding = language_server!().offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
@ -745,18 +753,11 @@ pub async fn handle_language_server_message(
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
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
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
language_server.offset_encoding(),
offset_encoding,
) {
start
} else {
@ -764,11 +765,9 @@ pub async fn handle_language_server_message(
return None;
};
let end = if let Some(end) = lsp_pos_to_pos(
text,
diagnostic.range.end,
language_server.offset_encoding(),
) {
let end = if let Some(end) =
lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
{
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
@ -807,14 +806,19 @@ pub async fn handle_language_server_message(
None => None,
};
let tags = if let Some(ref tags) = diagnostic.tags {
let new_tags = tags.iter().filter_map(|tag| {
match *tag {
lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
_ => None
}
}).collect();
let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags
.iter()
.filter_map(|tag| match *tag {
lsp::DiagnosticTag::DEPRECATED => {
Some(DiagnosticTag::Deprecated)
}
lsp::DiagnosticTag::UNNECESSARY => {
Some(DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();
new_tags
} else {
@ -830,25 +834,40 @@ pub async fn handle_language_server_message(
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
language_server_id: server_id,
})
})
.collect();
doc.set_diagnostics(diagnostics);
doc.replace_diagnostics(diagnostics, server_id);
}
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
let mut diagnostics = params
.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
// 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.
self.editor
.diagnostics
.insert(params.uri, params.diagnostics);
match self.editor.diagnostics.entry(params.uri) {
Entry::Occupied(o) => {
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) => {
log::warn!("unhandled window/showMessage: {:?}", params);
@ -950,10 +969,8 @@ pub async fn handle_language_server_message(
.editor
.documents_mut()
.filter_map(|doc| {
if doc.language_server().map(|server| server.id())
== Some(server_id)
{
doc.set_diagnostics(Vec::new());
if doc.supports_language_server(server_id) {
doc.clear_diagnostics(server_id);
doc.url()
} else {
None
@ -1029,31 +1046,21 @@ pub async fn handle_language_server_message(
}))
}
Ok(MethodCall::WorkspaceFolders) => {
let language_server =
self.editor.language_servers.get_by_id(server_id).unwrap();
Ok(json!(&*language_server.workspace_folders().await))
Ok(json!(&*language_server!().workspace_folders().await))
}
Ok(MethodCall::WorkspaceConfiguration(params)) => {
let language_server = language_server!();
let result: Vec<_> = params
.items
.iter()
.map(|item| {
let mut config = match &item.scope_uri {
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()?,
};
let mut config = language_server.config()?;
if let Some(section) = item.section.as_ref() {
for part in section.split('.') {
config = config.get(part)?;
// for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
if !section.is_empty() {
for part in section.split('.') {
config = config.get(part)?;
}
}
}
Some(config)
@ -1074,15 +1081,7 @@ pub async fn handle_language_server_message(
}
};
let 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;
}
};
tokio::spawn(language_server.reply(id, reply));
tokio::spawn(language_server!().reply(id, reply));
}
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
}

View File

@ -23,6 +23,7 @@
regex::{self, Regex, RegexBuilder},
search::{self, CharMatcher},
selection, shellwords, surround,
syntax::LanguageServerFeature,
text_annotations::TextAnnotations,
textobject,
tree_sitter::Node,
@ -54,13 +55,13 @@
job::Callback,
keymap::ReverseKeymap,
ui::{
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, FilePicker, Picker,
Popup, Prompt, PromptEvent,
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem,
FilePicker, Picker, Popup, Prompt, PromptEvent,
},
};
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::HashSet, num::NonZeroUsize};
@ -3029,7 +3030,7 @@ fn exit_select_mode(cx: &mut Context) {
fn goto_first_diag(cx: &mut Context) {
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),
None => return,
};
@ -3038,7 +3039,7 @@ fn goto_first_diag(cx: &mut Context) {
fn goto_last_diag(cx: &mut Context) {
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),
None => return,
};
@ -3054,10 +3055,9 @@ fn goto_next_diag(cx: &mut Context) {
.cursor(doc.text().slice(..));
let diag = doc
.diagnostics()
.iter()
.shown_diagnostics()
.find(|diag| diag.range.start > cursor_pos)
.or_else(|| doc.diagnostics().first());
.or_else(|| doc.shown_diagnostics().next());
let selection = match diag {
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(..));
let diag = doc
.diagnostics()
.iter()
.shown_diagnostics()
.rev()
.find(|diag| diag.range.start < cursor_pos)
.or_else(|| doc.diagnostics().last());
.or_else(|| doc.shown_diagnostics().last());
let selection = match diag {
// NOTE: the selection is reversed because we're jumping to the
@ -3234,23 +3233,19 @@ fn language_server_completion(cx: &mut Context, ch: char) {
use helix_lsp::lsp;
// if ch matches completion char, trigger completion
let doc = doc_mut!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let trigger_completion = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.any(|ls| {
// TODO: what if trigger is multiple chars long
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| trigger.contains(ch)))
});
let capabilities = language_server.capabilities();
if let Some(lsp::CompletionOptions {
trigger_characters: Some(triggers),
..
}) = &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);
}
if trigger_completion {
cx.editor.clear_idle_timer();
super::completion(cx);
}
}
@ -3258,12 +3253,12 @@ fn signature_help(cx: &mut Context, ch: char) {
use helix_lsp::lsp;
// if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor);
// The language_server!() macro is not used here since it will
// print an "LSP not active for current buffer" message on
// every keypress.
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.next()
else {
return;
};
let capabilities = language_server.capabilities();
@ -4046,55 +4041,60 @@ fn format_selections(cx: &mut Context) {
use helix_lsp::{lsp, util::range_to_lsp_range};
let (view, doc) = current!(cx.editor);
let view_id = view.id;
// via lsp if available
// TODO: else via tree-sitter indentation calculations
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let ranges: Vec<lsp::Range> = doc
.selection(view.id)
.iter()
.map(|range| range_to_lsp_range(doc.text(), *range, language_server.offset_encoding()))
.collect();
if ranges.len() != 1 {
if doc.selection(view_id).len() != 1 {
cx.editor
.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
.selection(view_id)
.iter()
.map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
.collect();
// TODO: handle fails
// TODO: concurrent map over all ranges
let range = ranges[0];
let request = match language_server.text_document_range_formatting(
doc.identifier(),
range,
lsp::FormattingOptions::default(),
None,
) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support range formatting");
return;
}
};
let future = language_server
.text_document_range_formatting(
doc.identifier(),
range,
lsp::FormattingOptions::default(),
None,
)
.unwrap();
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(
doc.text(),
edits,
language_server.offset_encoding(),
);
let transaction =
helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
doc.apply(&transaction, view.id);
doc.apply(&transaction, view_id);
}
fn join_selections_impl(cx: &mut Context, select_space: bool) {
@ -4231,21 +4231,46 @@ pub fn completion(cx: &mut Context) {
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 cursor = savepoint.cursor();
let pos = pos_to_lsp_pos(&text, cursor, offset_encoding);
let mut seen_language_servers = HashSet::new();
let future = match language_server.completion(doc.identifier(), pos, None) {
Some(future) => future,
None => return,
};
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();
anyhow::Ok(items)
}
})
.collect();
// setup a channel that allows the request to be canceled
let (tx, rx) = oneshot::channel();
@ -4254,12 +4279,20 @@ pub fn completion(cx: &mut Context) {
// and the associated request is automatically dropped
cx.editor.completion_request_handle = Some(tx);
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! {
biased;
_ = rx => {
Ok(serde_json::Value::Null)
Ok(Vec::new())
}
res = future => {
res = items_future => {
res
}
}
@ -4293,9 +4326,9 @@ pub fn completion(cx: &mut Context) {
},
));
cx.callback(
future,
move |editor, compositor, response: Option<lsp::CompletionResponse>| {
cx.jobs.callback(async move {
let items = future.await?;
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
@ -4306,16 +4339,6 @@ pub fn completion(cx: &mut Context) {
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() {
// editor.set_error("No completion available");
return;
@ -4326,7 +4349,6 @@ pub fn completion(cx: &mut Context) {
editor,
savepoint,
items,
offset_encoding,
start_offset,
trigger_offset,
size,
@ -4340,8 +4362,9 @@ pub fn completion(cx: &mut Context) {
{
compositor.remove(SignatureHelp::ID);
}
},
);
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
}
// comments
@ -5141,7 +5164,7 @@ async fn shell_impl_async(
helix_view::document::to_writer(&mut stdin, (encoding::UTF_8, false), &input)
.await?;
}
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
});
let (output, _) = tokio::join! {
process.wait_with_output(),

View File

@ -1,4 +1,4 @@
use futures_util::FutureExt;
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
use helix_lsp::{
block_on,
lsp::{
@ -6,8 +6,10 @@
NumberOrString,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
OffsetEncoding,
Client, OffsetEncoding,
};
use serde_json::Value;
use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
@ -15,7 +17,9 @@
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, text_annotations::InlineAnnotation, Selection};
use helix_core::{
path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
};
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
editor::Action,
@ -25,6 +29,7 @@
use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker,
Popup, PromptEvent,
@ -32,25 +37,34 @@
};
use std::{
cmp::Ordering, collections::BTreeMap, fmt::Write, future::Future, path::PathBuf, sync::Arc,
cmp::Ordering,
collections::{BTreeMap, HashSet},
fmt::Write,
future::Future,
path::PathBuf,
sync::Arc,
};
/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
/// in a context where the editor automatically queries the LSP
/// (instead of when the user explicitly does so via a keybind like
/// `gd`) will spam the "LSP inactive" status message confusingly.
/// Gets the first language server that is attached to a document which supports a specific feature.
/// If there is no configured language server that supports the feature, this displays a status message.
/// Using this macro in a context where the editor automatically queries the LSP
/// (instead of when the user explicitly does so via a keybind like `gd`)
/// will spam the "No configured language server supports <feature>" status message confusingly.
#[macro_export]
macro_rules! language_server {
($editor:expr, $doc:expr) => {
match $doc.language_server() {
macro_rules! language_server_with_feature {
($editor:expr, $doc:expr, $feature:expr) => {{
let language_server = $doc.language_servers_with_feature($feature).next();
match language_server {
Some(language_server) => language_server,
None => {
$editor.set_status("Language server not active for current buffer");
$editor.set_status(format!(
"No configured language server supports {}",
$feature
));
return;
}
}
};
}};
}
impl ui::menu::Item for lsp::Location {
@ -87,20 +101,30 @@ fn format(&self, cwdir: &Self::Data) -> Row {
}
}
impl ui::menu::Item for lsp::SymbolInformation {
struct SymbolInformationItem {
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for SymbolInformationItem {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
if current_doc_path.as_ref() == Some(&self.location.uri) {
self.name.as_str().into()
if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
self.symbol.name.as_str().into()
} else {
match self.location.uri.to_file_path() {
match self.symbol.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
format!("{} ({})", &self.name, get_relative_path.to_string_lossy()).into()
format!(
"{} ({})",
&self.symbol.name,
get_relative_path.to_string_lossy()
)
.into()
}
Err(_) => format!("{} ({})", &self.name, &self.location.uri).into(),
Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
}
}
}
@ -116,6 +140,7 @@ struct DiagnosticStyles {
struct PickerDiagnostic {
url: lsp::Url,
diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for PickerDiagnostic {
@ -211,21 +236,19 @@ fn jump_to_location(
align_view(doc, view, Align::Center);
}
fn sym_picker(
symbols: Vec<lsp::SymbolInformation>,
current_path: Option<lsp::Url>,
offset_encoding: OffsetEncoding,
) -> FilePicker<lsp::SymbolInformation> {
type SymbolPicker = FilePicker<SymbolInformationItem>;
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
FilePicker::new(
symbols,
current_path.clone(),
move |cx, symbol, action| {
move |cx, item, action| {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
if current_path.as_ref() != Some(&symbol.location.uri) {
let uri = &symbol.location.uri;
if current_path.as_ref() != Some(&item.symbol.location.uri) {
let uri = &item.symbol.location.uri;
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
@ -245,7 +268,7 @@ fn sym_picker(
let (view, doc) = current!(cx.editor);
if let Some(range) =
lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding)
{
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
@ -253,7 +276,7 @@ fn sym_picker(
align_view(doc, view, Align::Center);
}
},
move |_editor, symbol| Some(location_to_file_location(&symbol.location)),
move |_editor, item| Some(location_to_file_location(&item.symbol.location)),
)
.truncate_start(false)
}
@ -266,10 +289,9 @@ enum DiagnosticsFormat {
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
current_path: Option<lsp::Url>,
format: DiagnosticsFormat,
offset_encoding: OffsetEncoding,
) -> FilePicker<PickerDiagnostic> {
// TODO: drop current_path comparison and instead use workspace: bool flag?
@ -277,11 +299,15 @@ fn diag_picker(
let mut flat_diag = Vec::new();
for (url, diags) in diagnostics {
flat_diag.reserve(diags.len());
for diag in diags {
flat_diag.push(PickerDiagnostic {
url: url.clone(),
diag,
});
for (diag, ls) in diags {
if let Some(ls) = cx.editor.language_server_by_id(ls) {
flat_diag.push(PickerDiagnostic {
url: url.clone(),
diag,
offset_encoding: ls.offset_encoding(),
});
}
}
}
@ -295,7 +321,13 @@ fn diag_picker(
FilePicker::new(
flat_diag,
(styles, format),
move |cx, PickerDiagnostic { url, diag }, action| {
move |cx,
PickerDiagnostic {
url,
diag,
offset_encoding,
},
action| {
if current_path.as_ref() == Some(url) {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
@ -306,14 +338,14 @@ fn diag_picker(
let (view, doc) = current!(cx.editor);
if let Some(range) = lsp_range_to_range(doc.text(), diag.range, offset_encoding) {
if let Some(range) = lsp_range_to_range(doc.text(), diag.range, *offset_encoding) {
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(range.head, range.anchor));
align_view(doc, view, Align::Center);
}
},
move |_editor, PickerDiagnostic { url, diag }| {
move |_editor, PickerDiagnostic { url, diag, .. }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
},
@ -323,126 +355,154 @@ fn diag_picker(
pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
list: &mut Vec<lsp::SymbolInformation>,
list: &mut Vec<SymbolInformationItem>,
file: &lsp::TextDocumentIdentifier,
symbol: lsp::DocumentSymbol,
offset_encoding: OffsetEncoding,
) {
#[allow(deprecated)]
list.push(lsp::SymbolInformation {
name: symbol.name,
kind: symbol.kind,
tags: symbol.tags,
deprecated: symbol.deprecated,
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
container_name: None,
list.push(SymbolInformationItem {
symbol: lsp::SymbolInformation {
name: symbol.name,
kind: symbol.kind,
tags: symbol.tags,
deprecated: symbol.deprecated,
location: lsp::Location::new(file.uri.clone(), symbol.selection_range),
container_name: None,
},
offset_encoding,
});
for child in symbol.children.into_iter().flatten() {
nested_to_flat(list, file, child);
nested_to_flat(list, file, child, offset_encoding);
}
}
let doc = doc!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let current_url = doc.url();
let offset_encoding = language_server.offset_encoding();
let mut seen_language_servers = HashSet::new();
let future = match language_server.document_symbols(doc.identifier()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support document symbols");
return;
}
};
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let request = language_server.document_symbols(doc.identifier()).unwrap();
let offset_encoding = language_server.offset_encoding();
let doc_id = doc.identifier();
cx.callback(
future,
move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| {
if let Some(symbols) = response {
async move {
let json = request.await?;
let response: Option<lsp::DocumentSymbolResponse> = serde_json::from_value(json)?;
let symbols = match response {
Some(symbols) => symbols,
None => return anyhow::Ok(vec![]),
};
// lsp has two ways to represent symbols (flat/nested)
// convert the nested variant to flat, so that we have a homogeneous list
let symbols = match symbols {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
.into_iter()
.map(|symbol| SymbolInformationItem {
symbol,
offset_encoding,
})
.collect(),
lsp::DocumentSymbolResponse::Nested(symbols) => {
let doc = doc!(editor);
let mut flat_symbols = Vec::new();
for symbol in symbols {
nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
}
flat_symbols
}
};
let picker = sym_picker(symbols, current_url, offset_encoding);
compositor.push(Box::new(overlaid(picker)))
Ok(symbols)
}
},
)
})
.collect();
let current_url = doc.url();
if futures.is_empty() {
cx.editor
.set_error("No configured language server supports document symbols");
return;
}
cx.jobs.callback(async move {
let mut symbols = Vec::new();
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
symbols.append(&mut lsp_items);
}
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
compositor.push(Box::new(overlaid(picker)))
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
}
pub fn workspace_symbol_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let current_url = doc.url();
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let future = match language_server.workspace_symbols("".to_string()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support workspace symbols");
return;
let get_symbols = move |pattern: String, editor: &mut Editor| {
let doc = doc!(editor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
let offset_encoding = language_server.offset_encoding();
async move {
let json = request.await?;
let response =
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
.unwrap_or_default()
.into_iter()
.map(|symbol| SymbolInformationItem {
symbol,
offset_encoding,
})
.collect();
anyhow::Ok(response)
}
})
.collect();
if futures.is_empty() {
editor.set_error("No configured language server supports workspace symbols");
}
async move {
let mut symbols = Vec::new();
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
symbols.append(&mut lsp_items);
}
anyhow::Ok(symbols)
}
.boxed()
};
cx.callback(
future,
move |_editor, compositor, response: Option<Vec<lsp::SymbolInformation>>| {
let symbols = response.unwrap_or_default();
let picker = sym_picker(symbols, current_url, offset_encoding);
let get_symbols = |query: String, editor: &mut Editor| {
let doc = doc!(editor);
let language_server = match doc.language_server() {
Some(s) => s,
None => {
// This should not generally happen since the picker will not
// even open in the first place if there is no server.
return async move { Err(anyhow::anyhow!("LSP not active")) }.boxed();
}
};
let symbol_request = match language_server.workspace_symbols(query) {
Some(future) => future,
None => {
// This should also not happen since the language server must have
// supported workspace symbols before to reach this block.
return async move {
Err(anyhow::anyhow!(
"Language server does not support workspace symbols"
))
}
.boxed();
}
};
let current_url = doc.url();
let initial_symbols = get_symbols("".to_owned(), cx.editor);
let future = async move {
let json = symbol_request.await?;
let response: Option<Vec<lsp::SymbolInformation>> =
serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
};
future.boxed()
};
cx.jobs.callback(async move {
let symbols = initial_symbols.await?;
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlaid(dyn_picker)))
},
)
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
}
pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let language_server = language_server!(cx.editor, doc);
if let Some(current_url) = doc.url() {
let offset_encoding = language_server.offset_encoding();
let diagnostics = cx
.editor
.diagnostics
@ -454,7 +514,6 @@ pub fn diagnostics_picker(cx: &mut Context) {
[(current_url.clone(), diagnostics)].into(),
Some(current_url),
DiagnosticsFormat::HideSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlaid(picker)));
}
@ -462,24 +521,27 @@ pub fn diagnostics_picker(cx: &mut Context) {
pub fn workspace_diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let current_url = doc.url();
let offset_encoding = language_server.offset_encoding();
// TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
let diagnostics = cx.editor.diagnostics.clone();
let picker = diag_picker(
cx,
diagnostics,
current_url,
DiagnosticsFormat::ShowSourcePath,
offset_encoding,
);
cx.push_layer(Box::new(overlaid(picker)));
}
impl ui::menu::Item for lsp::CodeActionOrCommand {
struct CodeActionOrCommandItem {
lsp_item: lsp::CodeActionOrCommand,
language_server_id: usize,
}
impl ui::menu::Item for CodeActionOrCommandItem {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
match self {
match &self.lsp_item {
lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str().into(),
lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
}
@ -546,45 +608,42 @@ fn action_fixes_diagnostics(action: &CodeActionOrCommand) -> bool {
pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let selection_range = doc.selection(view.id).primary();
let offset_encoding = language_server.offset_encoding();
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
let mut seen_language_servers = HashSet::new();
let future = match language_server.code_actions(
doc.identifier(),
range,
// Filter and convert overlapping diagnostics
lsp::CodeActionContext {
diagnostics: doc
.diagnostics()
.iter()
.filter(|&diag| {
selection_range
.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
})
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
.collect(),
only: None,
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
},
) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support code actions");
return;
}
};
cx.callback(
future,
move |editor, compositor, response: Option<lsp::CodeActionResponse>| {
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::CodeAction)
.filter(|ls| seen_language_servers.insert(ls.id()))
// TODO this should probably already been filtered in something like "language_servers_with_feature"
.filter_map(|language_server| {
let offset_encoding = language_server.offset_encoding();
let language_server_id = language_server.id();
let range = range_to_lsp_range(doc.text(), selection_range, offset_encoding);
// Filter and convert overlapping diagnostics
let code_action_context = lsp::CodeActionContext {
diagnostics: doc
.diagnostics()
.iter()
.filter(|&diag| {
selection_range
.overlaps(&helix_core::Range::new(diag.range.start, diag.range.end))
})
.map(|diag| diagnostic_to_lsp_diagnostic(doc.text(), diag, offset_encoding))
.collect(),
only: None,
trigger_kind: Some(CodeActionTriggerKind::INVOKED),
};
let code_action_request =
language_server.code_actions(doc.identifier(), range, code_action_context)?;
Some((code_action_request, language_server_id))
})
.map(|(request, ls_id)| async move {
let json = request.await?;
let response: Option<lsp::CodeActionResponse> = serde_json::from_value(json)?;
let mut actions = match response {
Some(a) => a,
None => return,
None => return anyhow::Ok(Vec::new()),
};
// remove disabled code actions
@ -596,11 +655,6 @@ pub fn code_action(cx: &mut Context) {
)
});
if actions.is_empty() {
editor.set_status("No code actions available");
return;
}
// Sort codeactions into a useful order. This behaviour is only partially described in the LSP spec.
// Many details are modeled after vscode because language servers are usually tested against it.
// VScode sorts the codeaction two times:
@ -636,18 +690,51 @@ pub fn code_action(cx: &mut Context) {
.reverse()
});
let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| {
Ok(actions
.into_iter()
.map(|lsp_item| CodeActionOrCommandItem {
lsp_item,
language_server_id: ls_id,
})
.collect())
})
.collect();
if futures.is_empty() {
cx.editor
.set_error("No configured language server supports code actions");
return;
}
cx.jobs.callback(async move {
let mut actions = Vec::new();
// TODO if one code action request errors, all other requests are ignored (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
actions.append(&mut lsp_items);
}
let call = move |editor: &mut Editor, compositor: &mut Compositor| {
if actions.is_empty() {
editor.set_error("No code actions available");
return;
}
let mut picker = ui::Menu::new(actions, (), move |editor, action, event| {
if event != PromptEvent::Validate {
return;
}
// always present here
let code_action = code_action.unwrap();
let action = action.unwrap();
let Some(language_server) = editor.language_server_by_id(action.language_server_id) else {
editor.set_error("Language Server disappeared");
return;
};
let offset_encoding = language_server.offset_encoding();
match code_action {
match &action.lsp_item {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
execute_lsp_command(editor, command.clone());
execute_lsp_command(editor, action.language_server_id, command.clone());
}
lsp::CodeActionOrCommand::CodeAction(code_action) => {
log::debug!("code action: {:?}", code_action);
@ -659,7 +746,7 @@ pub fn code_action(cx: &mut Context) {
// if code action provides both edit and command first the edit
// should be applied and then the command
if let Some(command) = &code_action.command {
execute_lsp_command(editor, command.clone());
execute_lsp_command(editor, action.language_server_id, command.clone());
}
}
}
@ -668,8 +755,10 @@ pub fn code_action(cx: &mut Context) {
let popup = Popup::new("code-action", picker).with_scrollbar(false);
compositor.replace_or_push("code-action", popup);
},
)
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
}
impl ui::menu::Item for lsp::Command {
@ -679,13 +768,13 @@ fn format(&self, _data: &Self::Data) -> Row {
}
}
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
let doc = doc!(editor);
let language_server = language_server!(editor, doc);
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
let future = match language_server.command(cmd) {
let future = match editor
.language_server_by_id(language_server_id)
.and_then(|language_server| language_server.command(cmd))
{
Some(future) => future,
None => {
editor.set_error("Language server does not support executing commands");
@ -977,21 +1066,17 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo
}
}
pub fn goto_declaration(cx: &mut Context) {
fn goto_single_impl<P, F>(cx: &mut Context, feature: LanguageServerFeature, request_provider: P)
where
P: Fn(&Client, lsp::Position, lsp::TextDocumentIdentifier) -> Option<F>,
F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send,
{
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let language_server = language_server_with_feature!(cx.editor, doc, feature);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.goto_declaration(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-declaration");
return;
}
};
let future = request_provider(language_server, pos, doc.identifier()).unwrap();
cx.callback(
future,
@ -1002,102 +1087,56 @@ pub fn goto_declaration(cx: &mut Context) {
);
}
pub fn goto_declaration(cx: &mut Context) {
goto_single_impl(
cx,
LanguageServerFeature::GotoDeclaration,
|ls, pos, doc_id| ls.goto_declaration(doc_id, pos, None),
);
}
pub fn goto_definition(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.goto_definition(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-definition");
return;
}
};
cx.callback(
future,
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
let items = to_locations(response);
goto_impl(editor, compositor, items, offset_encoding);
},
goto_single_impl(
cx,
LanguageServerFeature::GotoDefinition,
|ls, pos, doc_id| ls.goto_definition(doc_id, pos, None),
);
}
pub fn goto_type_definition(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.goto_type_definition(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-type-definition");
return;
}
};
cx.callback(
future,
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
let items = to_locations(response);
goto_impl(editor, compositor, items, offset_encoding);
},
goto_single_impl(
cx,
LanguageServerFeature::GotoTypeDefinition,
|ls, pos, doc_id| ls.goto_type_definition(doc_id, pos, None),
);
}
pub fn goto_implementation(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.goto_implementation(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-implementation");
return;
}
};
cx.callback(
future,
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
let items = to_locations(response);
goto_impl(editor, compositor, items, offset_encoding);
},
goto_single_impl(
cx,
LanguageServerFeature::GotoImplementation,
|ls, pos, doc_id| ls.goto_implementation(doc_id, pos, None),
);
}
pub fn goto_reference(cx: &mut Context) {
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
// TODO could probably support multiple language servers,
// not sure if there's a real practical use case for this though
let language_server =
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::GotoReference);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.goto_reference(
doc.identifier(),
pos,
config.lsp.goto_reference_include_declaration,
None,
) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support goto-reference");
return;
}
};
let future = language_server
.goto_reference(
doc.identifier(),
pos,
config.lsp.goto_reference_include_declaration,
None,
)
.unwrap();
cx.callback(
future,
@ -1108,7 +1147,7 @@ pub fn goto_reference(cx: &mut Context) {
);
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
@ -1120,35 +1159,31 @@ pub fn signature_help(cx: &mut Context) {
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if was_manually_invoked {
cx.editor
.set_status("Language server not active for current buffer");
}
return;
}
};
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f,
None => {
if was_manually_invoked {
cx.editor
.set_error("Language server does not support signature-help");
}
return;
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
let future = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.find_map(|language_server| {
let pos = doc.position(view.id, language_server.offset_encoding());
language_server.text_document_signature_help(doc.identifier(), pos, None)
});
let Some(future) = future else {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual {
cx.editor.set_error("No configured language server supports signature-help");
}
return;
};
signature_help_impl_with_future(cx, future.boxed(), invoked);
}
pub fn signature_help_impl_with_future(
cx: &mut Context,
future: BoxFuture<'static, helix_lsp::Result<Value>>,
invoked: SignatureHelpInvoked,
) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
@ -1156,7 +1191,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| was_manually_invoked)
|| invoked == SignatureHelpInvoked::Manual)
{
return;
}
@ -1165,7 +1200,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
if !was_manually_invoked && editor.mode != Mode::Insert {
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
@ -1255,21 +1290,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
pub fn hover(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
// TODO support multiple language servers (merge UI somehow)
let language_server =
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::Hover);
// TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.text_document_hover(doc.identifier(), pos, None) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support hover");
return;
}
};
let pos = doc.position(view.id, language_server.offset_encoding());
let future = language_server
.text_document_hover(doc.identifier(), pos, None)
.unwrap();
cx.callback(
future,
@ -1349,7 +1378,11 @@ fn get_prefill_from_lsp_response(
}
}
fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
fn create_rename_prompt(
editor: &Editor,
prefill: String,
language_server_id: Option<usize>,
) -> Box<ui::Prompt> {
let prompt = ui::Prompt::new(
"rename-to:".into(),
None,
@ -1358,22 +1391,22 @@ fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
if event != PromptEvent::Validate {
return;
}
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.find(|ls| language_server_id.map_or(true, |id| id == ls.id()))
else {
cx.editor.set_error("No configured language server supports symbol renaming");
return;
};
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = language_server
.rename_symbol(doc.identifier(), pos, input.to_string())
.unwrap();
let future =
match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) {
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support symbol renaming");
return;
}
};
match block_on(future) {
Ok(edits) => {
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
@ -1387,21 +1420,28 @@ fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
Box::new(prompt)
}
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let offset_encoding = language_server.offset_encoding();
let (view, doc) = current_ref!(cx.editor);
if !language_server.supports_rename() {
cx.editor
.set_error("Language server does not support symbol renaming");
return;
}
let language_server_with_prepare_rename_support = doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.find(|ls| {
matches!(
ls.capabilities().rename_provider,
Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
..
}))
)
});
let pos = doc.position(view.id, offset_encoding);
match language_server.prepare_rename(doc.identifier(), pos) {
// Language server supports textDocument/prepareRename, use it.
Some(future) => cx.callback(
if let Some(language_server) = language_server_with_prepare_rename_support {
let ls_id = language_server.id();
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = language_server
.prepare_rename(doc.identifier(), pos)
.unwrap();
cx.callback(
future,
move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| {
let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response)
@ -1413,39 +1453,27 @@ fn create_rename_prompt(editor: &Editor, prefill: String) -> Box<ui::Prompt> {
}
};
let prompt = create_rename_prompt(editor, prefill);
let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
compositor.push(prompt);
},
),
// Language server does not support textDocument/prepareRename, fall back
// to word boundary selection.
None => {
let prefill = get_prefill_from_word_boundary(cx.editor);
let prompt = create_rename_prompt(cx.editor, prefill);
cx.push_layer(prompt);
}
};
);
} else {
let prefill = get_prefill_from_word_boundary(cx.editor);
let prompt = create_rename_prompt(cx.editor, prefill, None);
cx.push_layer(prompt);
}
}
pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let language_server =
language_server_with_feature!(cx.editor, doc, LanguageServerFeature::DocumentHighlight);
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = match language_server.text_document_document_highlight(doc.identifier(), pos, None)
{
Some(future) => future,
None => {
cx.editor
.set_error("Language server does not support document highlight");
return;
}
};
let future = language_server
.text_document_document_highlight(doc.identifier(), pos, None)
.unwrap();
cx.callback(
future,
@ -1455,8 +1483,6 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
_ => return,
};
let (view, doc) = current!(editor);
let language_server = language_server!(editor, doc);
let offset_encoding = language_server.offset_encoding();
let text = doc.text();
let pos = doc.selection(view.id).primary().head;
@ -1502,63 +1528,51 @@ fn compute_inlay_hints_for_view(
let view_id = view.id;
let doc_id = view.doc;
let language_server = doc.language_server()?;
let language_server = doc
.language_servers_with_feature(LanguageServerFeature::InlayHints)
.next()?;
let capabilities = language_server.capabilities();
let doc_text = doc.text();
let len_lines = doc_text.len_lines();
let (future, new_doc_inlay_hints_id) = match capabilities.inlay_hint_provider {
Some(
lsp::OneOf::Left(true)
| lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)),
) => {
let doc_text = doc.text();
let len_lines = doc_text.len_lines();
// Compute ~3 times the current view height of inlay hints, that way some scrolling
// will not show half the view with hints and half without while still being faster
// than computing all the hints for the full file (which could be dozens of time
// longer than the view is).
let view_height = view.inner_height();
let first_visible_line = doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
let first_line = first_visible_line.saturating_sub(view_height);
let last_line = first_visible_line
.saturating_add(view_height.saturating_mul(2))
.min(len_lines);
// Compute ~3 times the current view height of inlay hints, that way some scrolling
// will not show half the view with hints and half without while still being faster
// than computing all the hints for the full file (which could be dozens of time
// longer than the view is).
let view_height = view.inner_height();
let first_visible_line =
doc_text.char_to_line(view.offset.anchor.min(doc_text.len_chars()));
let first_line = first_visible_line.saturating_sub(view_height);
let last_line = first_visible_line
.saturating_add(view_height.saturating_mul(2))
.min(len_lines);
let new_doc_inlay_hint_id = DocumentInlayHintsId {
first_line,
last_line,
};
// Don't recompute the annotations in case nothing has changed about the view
if !doc.inlay_hints_oudated
&& doc
.inlay_hints(view_id)
.map_or(false, |dih| dih.id == new_doc_inlay_hint_id)
{
return None;
}
let doc_slice = doc_text.slice(..);
let first_char_in_range = doc_slice.line_to_char(first_line);
let last_char_in_range = doc_slice.line_to_char(last_line);
let range = helix_lsp::util::range_to_lsp_range(
doc_text,
helix_core::Range::new(first_char_in_range, last_char_in_range),
language_server.offset_encoding(),
);
(
language_server.text_document_range_inlay_hints(doc.identifier(), range, None),
new_doc_inlay_hint_id,
)
}
_ => return None,
let new_doc_inlay_hints_id = DocumentInlayHintsId {
first_line,
last_line,
};
// Don't recompute the annotations in case nothing has changed about the view
if !doc.inlay_hints_oudated
&& doc
.inlay_hints(view_id)
.map_or(false, |dih| dih.id == new_doc_inlay_hints_id)
{
return None;
}
let doc_slice = doc_text.slice(..);
let first_char_in_range = doc_slice.line_to_char(first_line);
let last_char_in_range = doc_slice.line_to_char(last_line);
let range = helix_lsp::util::range_to_lsp_range(
doc_text,
helix_core::Range::new(first_char_in_range, last_char_in_range),
language_server.offset_encoding(),
);
let offset_encoding = language_server.offset_encoding();
let callback = super::make_job_callback(
future?,
language_server.text_document_range_inlay_hints(doc.identifier(), range, None)?,
move |editor, _compositor, response: Option<Vec<lsp::InlayHint>>| {
// The config was modified or the window was closed while the request was in flight
if !editor.config().lsp.display_inlay_hints || editor.tree.try_get(view_id).is_none() {
@ -1572,8 +1586,8 @@ fn compute_inlay_hints_for_view(
};
// If we have neither hints nor an LSP, empty the inlay hints since they're now oudated
let (mut hints, offset_encoding) = match (response, doc.language_server()) {
(Some(h), Some(ls)) if !h.is_empty() => (h, ls.offset_encoding()),
let mut hints = match response {
Some(hints) if !hints.is_empty() => hints,
_ => {
doc.set_inlay_hints(
view_id,

View File

@ -1329,26 +1329,22 @@ fn lsp_workspace_command(
if event != PromptEvent::Validate {
return Ok(());
}
let (_, doc) = current!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
cx.editor
.set_status("Language server not active for current buffer");
return Ok(());
}
let doc = doc!(cx.editor);
let Some((language_server_id, options)) = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| {
ls.capabilities()
.execute_command_provider
.as_ref()
.map(|options| (ls.id(), options))
})
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() {
let commands = options
.commands
@ -1362,8 +1358,8 @@ fn lsp_workspace_command(
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), |cx, command, _action| {
execute_lsp_command(cx.editor, command.clone());
let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, language_server_id, command.clone());
});
compositor.push(Box::new(overlaid(picker)))
},
@ -1376,6 +1372,7 @@ fn lsp_workspace_command(
if options.commands.iter().any(|c| c == &command) {
execute_lsp_command(
cx.editor,
language_server_id,
helix_lsp::lsp::Command {
title: command.clone(),
arguments: None,
@ -1407,7 +1404,6 @@ fn lsp_restart(
.language_config()
.context("LSP not defined for the current document")?;
let scope = config.scope.clone();
cx.editor.language_servers.restart(
config,
doc.path(),
@ -1420,13 +1416,22 @@ fn lsp_restart(
.editor
.documents()
.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,
})
.collect();
for document_id in document_ids_to_refresh {
cx.editor.refresh_language_server(document_id);
cx.editor.refresh_language_servers(document_id);
}
Ok(())
@ -1441,22 +1446,18 @@ fn lsp_stop(
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
.language_server()
.map(|ls| ls.id())
.context("LSP not running for the current document")?;
for ls_name in &ls_shutdown_names {
cx.editor.language_servers.stop(ls_name);
let config = doc
.language_config()
.context("LSP not defined for the current document")?;
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());
for doc in cx.editor.documents_mut() {
if let Some(client) = doc.remove_language_server_by_name(ls_name) {
doc.clear_diagnostics(client.id());
}
}
}
@ -1850,7 +1851,7 @@ fn language(
doc.detect_indent_and_line_ending();
let id = doc.id();
cx.editor.refresh_language_server(id);
cx.editor.refresh_language_servers(id);
Ok(())
}
@ -2588,14 +2589,14 @@ fn clear_register(
TypableCommand {
name: "lsp-restart",
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,
signature: CommandSignature::none(),
},
TypableCommand {
name: "lsp-stop",
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,
signature: CommandSignature::none(),
},

View File

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

View File

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

View File

@ -33,7 +33,7 @@
use tui::{buffer::Buffer as Surface, text::Span};
use super::statusline;
use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
@ -650,7 +650,7 @@ pub fn render_diagnostics(
.primary()
.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
});
@ -953,20 +953,13 @@ pub fn set_completion(
&mut self,
editor: &mut Editor,
savepoint: Arc<SavePoint>,
items: Vec<helix_lsp::lsp::CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
size: Rect,
) -> Option<Rect> {
let mut completion = Completion::new(
editor,
savepoint,
items,
offset_encoding,
start_offset,
trigger_offset,
);
let mut completion =
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
if completion.is_empty() {
// skip if we got no completion results

View File

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

View File

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

View File

@ -6,7 +6,7 @@
use helix_core::auto_pairs::AutoPairs;
use helix_core::doc_formatter::TextFormat;
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::Range;
use helix_vcs::{DiffHandle, DiffProviderRegistry};
@ -179,8 +179,8 @@ pub struct Document {
version: i32, // should be usize?
pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
diff_handle: Option<DiffHandle>,
version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
@ -580,7 +580,7 @@ fn take_with<T, F>(mut_ref: &mut T, f: F)
*mut_ref = f(mem::take(mut_ref));
}
use helix_lsp::lsp;
use helix_lsp::{lsp, Client, LanguageServerName};
use url::Url;
impl Document {
@ -616,7 +616,7 @@ pub fn from(
last_saved_time: SystemTime::now(),
last_saved_revision: 0,
modified_since_accessed: false,
language_server: None,
language_servers: HashMap::new(),
diff_handle: None,
config,
version_control_head: None,
@ -730,10 +730,12 @@ pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterE
return Some(formatting_future.boxed());
};
let language_server = self.language_server()?;
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 request = language_server.text_document_formatting(
self.identifier(),
lsp::FormattingOptions {
@ -797,13 +799,12 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
if self.path.is_none() {
bail!("Can't save with no path set!");
}
self.path.as_ref().unwrap().clone()
}
};
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
let current_rev = self.get_current_revision();
@ -847,14 +848,13 @@ impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send
text: text.clone(),
};
if let Some(language_server) = language_server {
for (_, language_server) in language_servers {
if !language_server.is_initialized() {
return Ok(event);
}
if let Some(identifier) = identifier {
if let Some(identifier) = &identifier {
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
language_server.text_document_did_save(identifier.clone(), &text)
{
notification.await?;
}
@ -1004,11 +1004,6 @@ pub fn set_language_by_language_id(
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`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
// TODO: use a transaction?
@ -1159,7 +1154,7 @@ fn apply_impl(
if 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(
self.versioned_identifier(),
&old_doc,
@ -1415,18 +1410,13 @@ pub fn language_name(&self) -> Option<&str> {
.map(|language| language.language_id.as_str())
}
/// Language ID for the document. Either the `language-id` from the
/// `language-server` configuration, or the document language if no
/// `language-id` has been specified.
/// Language ID for the document. Either the `language-id`,
/// or the document language name if no `language-id` has been specified.
pub fn language_id(&self) -> Option<&str> {
let language_config = self.language.as_deref()?;
language_config
.language_server
.as_ref()?
.language_id
self.language_config()?
.language_server_language_id
.as_deref()
.or(Some(language_config.language_id.as_str()))
.or_else(|| self.language_name())
}
/// Corresponding [`LanguageConfiguration`].
@ -1439,10 +1429,45 @@ pub fn version(&self) -> i32 {
self.version
}
/// Language server if it has been initialized.
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
let server = self.language_server.as_deref()?;
server.is_initialized().then_some(server)
/// maintains the order as configured in the language_servers TOML array
pub fn language_servers(&self) -> 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() {
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> {
@ -1565,12 +1590,29 @@ pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator {
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
.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
/// language config with auto pairs configured, returns that;
/// otherwise, falls back to the global auto pairs config. If the global

View File

@ -818,7 +818,7 @@ pub struct Editor {
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>,
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 debugger: Option<dap::Client>,
@ -874,7 +874,7 @@ pub struct Editor {
/// times during rendering and should not be set by other functions.
pub cursor_cache: Cell<Option<Option<Position>>>,
/// 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
/// when the channel is dropped. That channel is stored
/// here. When a new completion request is sent this
@ -941,6 +941,7 @@ pub fn new(
syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>,
) -> Self {
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into();
@ -960,7 +961,7 @@ pub fn new(
macro_recording: None,
macro_replaying: Vec::new(),
theme: theme_loader.default(),
language_servers: helix_lsp::Registry::new(),
language_servers,
diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(),
debugger: None,
@ -1092,60 +1093,75 @@ fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
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
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_server(doc_id)
pub fn refresh_language_servers(&mut self, doc_id: DocumentId) -> Option<()> {
self.launch_language_servers(doc_id)
}
/// 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 {
return None;
}
// 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 config = doc.config.load();
let root_dirs = &config.workspace_lsp_roots;
// try to find a language server based on the language name
let language_server = lang.as_ref().and_then(|language| {
// try to find language servers based on the language name
let language_servers = lang.as_ref().and_then(|language| {
self.language_servers
.get(language, path.as_ref(), root_dirs, config.lsp.snippets)
.map_err(|e| {
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
"Failed to initialize the language servers for `{}` {{ {} }}",
language.scope(),
e
)
})
.ok()
.flatten()
});
let doc = self.document_mut(doc_id)?;
let doc_url = doc.url()?;
if let Some(language_servers) = language_servers {
let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
if let Some(language_server) = language_server {
// 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()));
}
// only spawn new language servers if the servers aren't the same
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
tokio::spawn(language_server.text_document_did_open(
doc_url,
doc_url.clone(),
doc.version(),
doc.text(),
language_id,
language_id.clone(),
));
doc.set_language_server(Some(language_server));
}
doc.language_servers = language_servers;
}
Some(())
}
@ -1338,7 +1354,7 @@ pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error>
doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
let id = self.new_document(doc);
let _ = self.launch_language_server(id);
let _ = self.launch_language_servers(id);
id
};
@ -1368,7 +1384,7 @@ pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(),
// This will also disallow any follow-up writes
self.saves.remove(&doc_id);
if let Some(language_server) = doc.language_server() {
for language_server in doc.language_servers() {
// TODO: track error
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}

View File

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

View File

@ -1,6 +1,143 @@
# Language support configuration.
# See the languages documentation: https://docs.helix-editor.com/master/languages.html
[language-server]
awk-language-server = { command = "awk-language-server" }
bash-language-server = { command = "bash-language-server", args = ["start"] }
bass = { command = "bass", args = ["--lsp"] }
bicep-langserver = { command = "bicep-langserver" }
cl-lsp = { command = "cl-lsp", args = [ "stdio" ] }
clangd = { command = "clangd" }
clojure-lsp = { command = "clojure-lsp" }
cmake-language-server = { command = "cmake-language-server" }
crystalline = { command = "crystalline", args = ["--stdio"] }
cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
cuelsp = { command = "cuelsp" }
dart = { command = "dart", args = ["language-server", "--client-id=helix"] }
dhall-lsp-server = { command = "dhall-lsp-server" }
docker-langserver = { command = "docker-langserver", args = ["--stdio"] }
dot-language-server = { command = "dot-language-server", args = ["--stdio"] }
elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } }
elm-language-server = { command = "elm-language-server" }
elvish = { command = "elvish", args = ["-lsp"] }
erlang-ls = { command = "erlang_ls" }
forc = { command = "forc", args = ["lsp"] }
fortls = { command = "fortls", args = ["--lowercase_intrinsics"] }
gleam = { command = "gleam", args = ["lsp"] }
haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
idris2-lsp = { command = "idris2-lsp" }
intelephense = { command = "intelephense", args = ["--stdio"] }
jdtls = { command = "jdtls" }
jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] }
kotlin-language-server = { command = "kotlin-language-server" }
lean = { command = "lean", args = [ "--server" ] }
markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
marksman = { command = "marksman", args = ["server"] }
metals = { command = "metals", config = { "isHttpEnabled" = true } }
mint = { command = "mint", args = ["ls"] }
nil = { command = "nil" }
nimlangserver = { command = "nimlangserver" }
nls = { command = "nls" }
ocamllsp = { command = "ocamllsp" }
ols = { command = "ols", args = [] }
omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] }
openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] }
pasls = { command = "pasls", args = [] }
perlnavigator = { command = "perlnavigator", args= ["--stdio"] }
prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] }
purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] }
pylsp = { command = "pylsp" }
qmlls = { command = "qmlls" }
r = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
racket = { command = "racket", args = ["-l", "racket-langserver"] }
regols = { command = "regols" }
rescript-language-server = { command = "rescript-language-server", args = ["--stdio"] }
robotframework_ls = { command = "robotframework_ls" }
serve-d = { command = "serve-d" }
slint-lsp = { command = "slint-lsp", args = [] }
solargraph = { command = "solargraph", args = ["stdio"] }
solc = { command = "solc", args = ["--lsp"] }
sourcekit-lsp = { command = "sourcekit-lsp" }
svelteserver = { command = "svelteserver", args = ["--stdio"] }
svlangserver = { command = "svlangserver", args = [] }
swipl = { command = "swipl", args = [ "-g", "use_module(library(lsp_server))", "-g", "lsp_server:main", "-t", "halt", "--", "stdio" ] }
taplo = { command = "taplo", args = ["lsp", "stdio"] }
terraform-ls = { command = "terraform-ls", args = ["serve"] }
texlab = { command = "texlab" }
vala-language-server = { command = "vala-language-server" }
vhdl_ls = { command = "vhdl_ls", args = [] }
vlang-language-server = { command = "v", args = ["ls"] }
vscode-css-language-server = { command = "vscode-css-language-server", args = ["--stdio"], config = { "provideFormatter" = true }}
vscode-html-language-server = { command = "vscode-html-language-server", args = ["--stdio"], config = { provideFormatter = true } }
vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } }
vuels = { command = "vls" }
wgsl_analyzer = { command = "wgsl_analyzer" }
yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
zls = { command = "zls" }
[language-server.lua-language-server]
command = "lua-language-server"
[language-server.lua-language-server.config.Lua.hint]
enable = true
arrayIndex = "Enable"
setType = true
paramName = "All"
paramType = true
await = true
[language-server.gopls]
command = "gopls"
[language-server.gopls.config.hints]
assignVariableTypes = true
compositeLiteralFields = true
constantValues = true
functionTypeParameters = true
parameterNames = true
rangeVariableTypes = true
[language-server.rust-analyzer]
command = "rust-analyzer"
[language-server.rust-analyzer.config]
inlayHints.bindingModeHints.enable = false
inlayHints.closingBraceHints.minLines = 10
inlayHints.closureReturnTypeHints.enable = "with_block"
inlayHints.discriminantHints.enable = "fieldless"
inlayHints.lifetimeElisionHints.enable = "skip_trivial"
inlayHints.typeHints.hideClosureInitialization = false
[language-server.typescript-language-server]
command = "typescript-language-server"
args = ["--stdio"]
config.hostInfo = "helix"
[language-server.typescript-language-server.config.typescript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[language-server.typescript-language-server.config.javascript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[language]]
name = "rust"
scope = "source.rust"
@ -9,7 +146,7 @@ file-types = ["rs"]
roots = ["Cargo.toml", "Cargo.lock"]
auto-format = true
comment-token = "//"
language-server = { command = "rust-analyzer" }
language-servers = [ "rust-analyzer" ]
indent = { tab-width = 4, unit = " " }
[language.auto-pairs]
@ -19,14 +156,6 @@ indent = { tab-width = 4, unit = " " }
'"' = '"'
'`' = '`'
[language.config]
inlayHints.bindingModeHints.enable = false
inlayHints.closingBraceHints.minLines = 10
inlayHints.closureReturnTypeHints.enable = "with_block"
inlayHints.discriminantHints.enable = "fieldless"
inlayHints.lifetimeElisionHints.enable = "skip_trivial"
inlayHints.typeHints.hideClosureInitialization = false
[language.debugger]
name = "lldb-vscode"
transport = "stdio"
@ -65,7 +194,7 @@ name = "sway"
scope = "source.sway"
injection-regex = "sway"
file-types = ["sw"]
language-server = { command = "forc", args = ["lsp"] }
language-servers = [ "forc" ]
roots = ["Forc.toml", "Forc.lock"]
indent = { tab-width = 4, unit = " " }
comment-token = "//"
@ -81,7 +210,7 @@ injection-regex = "toml"
file-types = ["toml", "poetry.lock"]
roots = []
comment-token = "#"
language-server = { command = "taplo", args = ["lsp", "stdio"] }
language-servers = [ "taplo" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -95,7 +224,7 @@ injection-regex = "awk"
file-types = ["awk", "gawk", "nawk", "mawk"]
roots = []
comment-token = "#"
language-server = { command = "awk-language-server" }
language-servers = [ "awk-language-server" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -123,8 +252,7 @@ file-types = ["ex", "exs", "mix.lock"]
shebangs = ["elixir"]
roots = ["mix.exs", "mix.lock"]
comment-token = "#"
language-server = { command = "elixir-ls" }
config = { elixirLS.dialyzerEnabled = false }
language-servers = [ "elixir-ls" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -153,7 +281,7 @@ file-types = ["mint"]
shebangs = []
roots = []
comment-token = "//"
language-server = { command = "mint", args = ["ls"] }
language-servers = [ "mint" ]
indent = { tab-width = 2, unit = " " }
[[language]]
@ -162,9 +290,8 @@ scope = "source.json"
injection-regex = "json"
file-types = ["json", "jsonc", "arb", "ipynb", "geojson"]
roots = []
language-server = { command = "vscode-json-language-server", args = ["--stdio"] }
language-servers = [ "vscode-json-language-server" ]
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -178,7 +305,7 @@ injection-regex = "c"
file-types = ["c"] # TODO: ["h"]
roots = []
comment-token = "//"
language-server = { command = "clangd" }
language-servers = [ "clangd" ]
indent = { tab-width = 2, unit = " " }
[language.debugger]
@ -215,7 +342,7 @@ injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"]
roots = []
comment-token = "//"
language-server = { command = "clangd" }
language-servers = [ "clangd" ]
indent = { tab-width = 2, unit = " " }
[language.debugger]
@ -253,7 +380,7 @@ roots = ["shard.yml", "shard.lock"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
grammar = "ruby"
language-server = { command = "crystalline", args = ["--stdio"] }
language-servers = [ "crystalline" ]
[[language]]
name = "c-sharp"
@ -263,7 +390,7 @@ file-types = ["cs"]
roots = ["sln", "csproj"]
comment-token = "//"
indent = { tab-width = 4, unit = "\t" }
language-server = { command = "OmniSharp", args = [ "--languageserver" ] }
language-servers = [ "omnisharp" ]
[language.debugger]
name = "netcoredbg"
@ -296,18 +423,10 @@ file-types = ["go"]
roots = ["go.work", "go.mod"]
auto-format = true
comment-token = "//"
language-server = { command = "gopls" }
language-servers = [ "gopls" ]
# TODO: gopls needs utf-8 offsets?
indent = { tab-width = 4, unit = "\t" }
[language.config.hints]
assignVariableTypes = true
compositeLiteralFields = true
constantValues = true
functionTypeParameters = true
parameterNames = true
rangeVariableTypes = true
[language.debugger]
name = "go"
transport = "tcp"
@ -351,7 +470,7 @@ file-types = ["go.mod"]
roots = []
auto-format = true
comment-token = "//"
language-server = { command = "gopls" }
language-servers = [ "gopls" ]
indent = { tab-width = 4, unit = "\t" }
[[grammar]]
@ -365,7 +484,7 @@ injection-regex = "gotmpl"
file-types = ["gotmpl"]
roots = []
comment-token = "//"
language-server = { command = "gopls" }
language-servers = [ "gopls" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -380,7 +499,7 @@ file-types = ["go.work"]
roots = []
auto-format = true
comment-token = "//"
language-server = { command = "gopls" }
language-servers = [ "gopls" ]
indent = { tab-width = 4, unit = "\t" }
[[grammar]]
@ -391,26 +510,15 @@ source = { git = "https://github.com/omertuc/tree-sitter-go-work", rev = "6dd9dd
name = "javascript"
scope = "source.js"
injection-regex = "(js|javascript)"
language-id = "javascript"
file-types = ["js", "mjs", "cjs"]
shebangs = ["node"]
roots = []
comment-token = "//"
# TODO: highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.javascript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[language.debugger]
name = "node-debug2"
transport = "stdio"
@ -431,48 +539,26 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-javascript", rev =
name = "jsx"
scope = "source.jsx"
injection-regex = "jsx"
language-id = "javascriptreact"
file-types = ["jsx"]
roots = []
comment-token = "//"
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascriptreact" }
language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " }
grammar = "javascript"
[language.config]
hostInfo = "helix"
[language.config.javascript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[language]]
name = "typescript"
scope = "source.ts"
injection-regex = "(ts|typescript)"
file-types = ["ts", "mts", "cts"]
language-id = "typescript"
shebangs = []
roots = []
# TODO: highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.typescript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[grammar]]
name = "typescript"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "typescript" }
@ -481,24 +567,13 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev =
name = "tsx"
scope = "source.tsx"
injection-regex = "(tsx)" # |typescript
language-id = "typescriptreact"
file-types = ["tsx"]
roots = []
# TODO: highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
language-servers = [ "typescript-language-server" ]
indent = { tab-width = 2, unit = " " }
[language.config]
hostInfo = "helix"
[language.config.typescript.inlayHints]
includeInlayEnumMemberValueHints = true
includeInlayFunctionLikeReturnTypeHints = true
includeInlayFunctionParameterTypeHints = true
includeInlayParameterNameHints = "all"
includeInlayParameterNameHintsWhenArgumentMatchesName = true
includeInlayPropertyDeclarationTypeHints = true
includeInlayVariableTypeHints = true
[[grammar]]
name = "tsx"
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "6aac031ad88dd6317f02ac0bb27d099a553a7d8c", subpath = "tsx" }
@ -509,9 +584,8 @@ scope = "source.css"
injection-regex = "css"
file-types = ["css", "scss"]
roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
language-servers = [ "vscode-css-language-server" ]
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -524,9 +598,8 @@ scope = "source.scss"
injection-regex = "scss"
file-types = ["scss"]
roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
language-servers = [ "vscode-css-language-server" ]
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -539,9 +612,8 @@ scope = "text.html.basic"
injection-regex = "html"
file-types = ["html"]
roots = []
language-server = { command = "vscode-html-language-server", args = ["--stdio"] }
language-servers = [ "vscode-html-language-server" ]
auto-format = true
config = { "provideFormatter" = true }
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -556,7 +628,7 @@ file-types = ["py","pyi","py3","pyw","ptl",".pythonstartup",".pythonrc","SConstr
shebangs = ["python"]
roots = []
comment-token = "#"
language-server = { command = "pylsp" }
language-servers = [ "pylsp" ]
# TODO: pyls needs utf-8 offsets
indent = { tab-width = 4, unit = " " }
@ -572,7 +644,7 @@ file-types = ["ncl"]
shebangs = []
roots = []
comment-token = "#"
language-server = { command = "nls" }
language-servers = [ "nls" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -587,7 +659,7 @@ file-types = ["nix"]
shebangs = []
roots = []
comment-token = "#"
language-server = { command = "nil" }
language-servers = [ "nil" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -602,7 +674,7 @@ file-types = ["rb", "rake", "rakefile", "irb", "gemfile", "gemspec", "Rakefile",
shebangs = ["ruby"]
roots = []
comment-token = "#"
language-server = { command = "solargraph", args = ["stdio"] }
language-servers = [ "solargraph" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -617,7 +689,7 @@ file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile
shebangs = ["sh", "bash", "dash", "zsh"]
roots = []
comment-token = "#"
language-server = { command = "bash-language-server", args = ["start"] }
language-servers = [ "bash-language-server" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -631,7 +703,7 @@ injection-regex = "php"
file-types = ["php", "inc"]
shebangs = ["php"]
roots = ["composer.json", "index.php"]
language-server = { command = "intelephense", args = ["--stdio"] }
language-servers = [ "intelephense" ]
indent = { tab-width = 4, unit = " " }
[[grammar]]
@ -657,7 +729,7 @@ injection-regex = "tex"
file-types = ["tex", "sty", "cls"]
roots = []
comment-token = "%"
language-server = { command = "texlab" }
language-servers = [ "texlab" ]
indent = { tab-width = 4, unit = "\t" }
[[grammar]]
@ -671,7 +743,7 @@ injection-regex = "bib"
file-types = ["bib"]
roots = []
comment-token = "%"
language-server = { command = "texlab" }
language-servers = [ "texlab" ]
indent = { tab-width = 4, unit = "\t" }
auto-format = true
@ -699,7 +771,7 @@ injection-regex = "lean"
file-types = ["lean"]
roots = [ "lakefile.lean" ]
comment-token = "--"
language-server = { command = "lean", args = [ "--server" ] }
language-servers = [ "lean" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -713,13 +785,7 @@ injection-regex = "julia"
file-types = ["jl"]
roots = ["Manifest.toml", "Project.toml"]
comment-token = "#"
language-server = { command = "julia", timeout = 60, args = [
"--startup-file=no",
"--history-file=no",
"--quiet",
"-e",
"using LanguageServer; runserver()",
] }
language-servers = [ "julia" ]
indent = { tab-width = 4, unit = " " }
[[grammar]]
@ -732,7 +798,7 @@ scope = "source.java"
injection-regex = "java"
file-types = ["java"]
roots = ["pom.xml", "build.gradle", "build.gradle.kts"]
language-server = { command = "jdtls" }
language-servers = [ "jdtls" ]
indent = { tab-width = 4, unit = " " }
[[grammar]]
@ -773,7 +839,7 @@ file-types = ["ml"]
shebangs = []
roots = []
comment-token = "(**)"
language-server = { command = "ocamllsp" }
language-servers = [ "ocamllsp" ]
indent = { tab-width = 2, unit = " " }
[language.auto-pairs]
@ -794,7 +860,7 @@ file-types = ["mli"]
shebangs = []
roots = []
comment-token = "(**)"
language-server = { command = "ocamllsp" }
language-servers = [ "ocamllsp" ]
indent = { tab-width = 2, unit = " " }
[language.auto-pairs]
@ -817,15 +883,7 @@ shebangs = ["lua"]
roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"]
comment-token = "--"
indent = { tab-width = 2, unit = " " }
language-server = { command = "lua-language-server", args = [] }
[language.config.Lua.hint]
enable = true
arrayIndex = "Enable"
setType = true
paramName = "All"
paramType = true
await = true
language-servers = [ "lua-language-server" ]
[[grammar]]
name = "lua"
@ -838,7 +896,7 @@ injection-regex = "svelte"
file-types = ["svelte"]
roots = []
indent = { tab-width = 2, unit = " " }
language-server = { command = "svelteserver", args = ["--stdio"] }
language-servers = [ "svelteserver" ]
[[grammar]]
name = "svelte"
@ -851,7 +909,7 @@ injection-regex = "vue"
file-types = ["vue"]
roots = ["package.json", "vue.config.js"]
indent = { tab-width = 2, unit = " " }
language-server = { command = "vls" }
language-servers = [ "vuels" ]
[[grammar]]
name = "vue"
@ -864,7 +922,7 @@ file-types = ["yml", "yaml"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "yaml-language-server", args = ["--stdio"] }
language-servers = [ "yaml-language-server" ]
injection-regex = "yml|yaml"
[[grammar]]
@ -878,7 +936,7 @@ injection-regex = "haskell"
file-types = ["hs", "hs-boot"]
roots = ["Setup.hs", "stack.yaml", "cabal.project"]
comment-token = "--"
language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
language-servers = [ "haskell-language-server" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -892,7 +950,7 @@ injection-regex = "purescript"
file-types = ["purs"]
roots = ["spago.dhall", "bower.json"]
comment-token = "--"
language-server = { command = "purescript-language-server", args = ["--stdio"] }
language-servers = [ "purescript-language-server" ]
indent = { tab-width = 2, unit = " " }
auto-format = true
formatter = { command = "purs-tidy", args = ["format"] }
@ -906,7 +964,7 @@ file-types = ["zig"]
roots = ["build.zig"]
auto-format = true
comment-token = "//"
language-server = { command = "zls" }
language-servers = [ "zls" ]
indent = { tab-width = 4, unit = " " }
formatter = { command = "zig" , args = ["fmt", "--stdin"] }
@ -944,10 +1002,7 @@ roots = []
file-types = ["pl", "prolog"]
shebangs = ["swipl"]
comment-token = "%"
language-server = { command = "swipl", args = [
"-g", "use_module(library(lsp_server))",
"-g", "lsp_server:main",
"-t", "halt", "--", "stdio"] }
language-servers = [ "swipl" ]
[[language]]
name = "tsq"
@ -969,7 +1024,7 @@ file-types = ["cmake", "CMakeLists.txt"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "cmake-language-server" }
language-servers = [ "cmake-language-server" ]
injection-regex = "cmake"
[[grammar]]
@ -1009,7 +1064,7 @@ file-types = ["pl", "pm", "t"]
shebangs = ["perl"]
roots = []
comment-token = "#"
language-server = { command = "perlnavigator", args= ["--stdio"] }
language-servers = [ "perlnavigator" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1023,7 +1078,7 @@ roots = []
file-types = ["rkt", "rktd", "rktl", "scrbl"]
shebangs = ["racket"]
comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] }
language-servers = [ "racket" ]
grammar = "scheme"
[[language]]
@ -1034,7 +1089,7 @@ file-types = ["lisp", "asd", "cl", "l", "lsp", "ny", "podsl", "sexp"]
shebangs = ["lisp", "sbcl", "ccl", "clisp", "ecl"]
comment-token = ";"
indent = { tab-width = 2, unit = " " }
language-server = { command = "cl-lsp", args = [ "stdio" ] }
language-servers = [ "cl-lsp" ]
grammar = "scheme"
[language.auto-pairs]
@ -1060,7 +1115,7 @@ scope = "source.wgsl"
file-types = ["wgsl"]
roots = []
comment-token = "//"
language-server = { command = "wgsl_analyzer" }
language-servers = [ "wgsl_analyzer" ]
indent = { tab-width = 4, unit = " " }
[[grammar]]
@ -1123,7 +1178,7 @@ scope = "source.md"
injection-regex = "md|markdown"
file-types = ["md", "markdown", "PULLREQ_EDITMSG"]
roots = [".marksman.toml"]
language-server = { command = "marksman", args=["server"] }
language-servers = [ "marksman" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1149,7 +1204,7 @@ file-types = ["dart"]
roots = ["pubspec.yaml"]
auto-format = true
comment-token = "//"
language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
language-servers = [ "dart" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1163,8 +1218,7 @@ roots = ["build.sbt", "build.sc", "build.gradle", "build.gradle.kts", "pom.xml",
file-types = ["scala", "sbt", "sc"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "metals" }
config = { "isHttpEnabled" = true }
language-servers = [ "metals" ]
[[grammar]]
name = "scala"
@ -1178,7 +1232,7 @@ roots = ["Dockerfile", "Containerfile"]
file-types = ["Dockerfile", "dockerfile", "Containerfile", "containerfile"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "docker-langserver", args = ["--stdio"] }
language-servers = [ "docker-langserver" ]
[[grammar]]
name = "dockerfile"
@ -1218,7 +1272,7 @@ roots = []
file-types = ["git-rebase-todo"]
injection-regex = "git-rebase"
comment-token = "#"
indent = { tab-width = 2, unit = " " }
indent = { tab-width = 2, unit = "y" }
[[grammar]]
name = "git-rebase"
@ -1294,7 +1348,7 @@ file-types = ["elm"]
roots = ["elm.json"]
auto-format = true
comment-token = "--"
language-server = { command = "elm-language-server" }
language-servers = [ "elm-language-server" ]
indent = { tab-width = 4, unit = " " }
[[grammar]]
@ -1320,7 +1374,7 @@ file-types = ["res"]
roots = ["bsconfig.json"]
auto-format = true
comment-token = "//"
language-server = { command = "rescript-language-server", args = ["--stdio"] }
language-servers = [ "rescript-language-server" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1336,7 +1390,7 @@ roots = ["rebar.config"]
shebangs = ["escript"]
comment-token = "%%"
indent = { tab-width = 4, unit = " " }
language-server = { command = "erlang_ls" }
language-servers = [ "erlang-ls" ]
[language.auto-pairs]
'(' = ')'
@ -1357,7 +1411,7 @@ file-types = ["kt", "kts"]
roots = ["settings.gradle", "settings.gradle.kts"]
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "kotlin-language-server" }
language-servers = [ "kotlin-language-server" ]
[[grammar]]
name = "kotlin"
@ -1367,11 +1421,12 @@ source = { git = "https://github.com/fwcd/tree-sitter-kotlin", rev = "a4f71eb9b8
name = "hcl"
scope = "source.hcl"
injection-regex = "(hcl|tf|nomad)"
language-id = "terraform"
file-types = ["hcl", "tf", "nomad"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform" }
language-servers = [ "terraform-ls" ]
auto-format = true
[[grammar]]
@ -1381,11 +1436,12 @@ source = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "3cb7
[[language]]
name = "tfvars"
scope = "source.tfvars"
language-id = "terraform-vars"
file-types = ["tfvars"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "terraform-ls", args = ["serve"], language-id = "terraform-vars" }
language-servers = [ "terraform-ls" ]
auto-format = true
grammar = "hcl"
@ -1409,7 +1465,7 @@ file-types = ["sol"]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "solc", args = ["--lsp"] }
language-servers = [ "solc" ]
[[grammar]]
name = "solidity"
@ -1423,7 +1479,7 @@ file-types = ["gleam"]
roots = ["gleam.toml"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "gleam", args = ["lsp"] }
language-servers = [ "gleam" ]
[[grammar]]
name = "gleam"
@ -1447,7 +1503,7 @@ file-types = ["robot", "resource"]
comment-token = "#"
roots = []
indent = { tab-width = 4, unit = " " }
language-server = { command = "robotframework_ls" }
language-servers = [ "robotframework_ls" ]
[[grammar]]
name = "robot"
@ -1462,7 +1518,7 @@ shebangs = ["r", "R"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] }
language-servers = [ "r" ]
[[grammar]]
name = "r"
@ -1476,7 +1532,7 @@ file-types = ["rmd", "Rmd"]
roots = []
indent = { tab-width = 2, unit = " " }
grammar = "markdown"
language-server = { command = "R", args = ["--slave", "-e", "languageserver::run()"] }
language-servers = [ "r" ]
[[language]]
name = "swift"
@ -1486,7 +1542,7 @@ file-types = ["swift"]
roots = [ "Package.swift" ]
comment-token = "//"
auto-format = true
language-server = { command = "sourcekit-lsp" }
language-servers = [ "sourcekit-lsp" ]
[[grammar]]
name = "swift"
@ -1533,8 +1589,7 @@ injection-regex = "heex"
file-types = ["heex"]
roots = ["mix.exs", "mix.lock"]
indent = { tab-width = 2, unit = " " }
language-server = { command = "elixir-ls" }
config = { elixirLS.dialyzerEnabled = false }
language-servers = [ "elixir-ls" ]
[[grammar]]
name = "heex"
@ -1605,7 +1660,7 @@ file-types = ["vala", "vapi"]
roots = []
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "vala-language-server" }
language-servers = [ "vala-language-server" ]
[[grammar]]
name = "vala"
@ -1670,7 +1725,7 @@ auto-format = false
scope = "source.odin"
file-types = ["odin"]
roots = ["ols.json"]
language-server = { command = "ols", args = [] }
language-servers = [ "ols" ]
comment-token = "//"
indent = { tab-width = 4, unit = "\t" }
@ -1721,7 +1776,7 @@ scope = "source.v"
file-types = ["v", "vv", "vsh"]
shebangs = ["v run"]
roots = ["v.mod"]
language-server = { command = "v", args = ["ls"] }
language-servers = [ "vlang-language-server" ]
auto-format = true
comment-token = "//"
indent = { tab-width = 4, unit = "\t" }
@ -1736,7 +1791,7 @@ scope = "source.verilog"
file-types = ["v", "vh", "sv", "svh"]
roots = []
comment-token = "//"
language-server = { command = "svlangserver", args = [] }
language-servers = [ "svlangserver" ]
indent = { tab-width = 2, unit = " " }
injection-regex = "verilog"
@ -1775,7 +1830,7 @@ injection-regex = "openscad"
file-types = ["scad"]
roots = []
comment-token = "//"
language-server = { command = "openscad-lsp", args = ["--stdio"] }
language-servers = [ "openscad-lsp" ]
indent = { tab-width = 2, unit = "\t" }
[[grammar]]
@ -1789,7 +1844,7 @@ injection-regex = "prisma"
file-types = ["prisma"]
roots = ["package.json"]
comment-token = "//"
language-server = { command = "prisma-language-server", args = ["--stdio"] }
language-servers = [ "prisma-language-server" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1803,7 +1858,7 @@ injection-regex = "(clojure|clj|edn|boot)"
file-types = ["clj", "cljs", "cljc", "clje", "cljr", "cljx", "edn", "boot"]
roots = ["project.clj", "build.boot", "deps.edn", "shadow-cljs.edn"]
comment-token = ";"
language-server = { command = "clojure-lsp" }
language-servers = [ "clojure-lsp" ]
indent = { tab-width = 2, unit = " " }
[[grammar]]
@ -1827,7 +1882,7 @@ file-types = ["elv"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "elvish", args = ["-lsp"] }
language-servers = [ "elvish" ]
grammar = "elvish"
[[grammar]]
@ -1843,7 +1898,7 @@ shebangs = []
roots = []
comment-token = "--"
indent = { tab-width = 2, unit = " " }
language-server = { command = "idris2-lsp" }
language-servers = [ "idris2-lsp" ]
[[language]]
name = "fortran"
@ -1853,7 +1908,7 @@ file-types = ["f", "for", "f90", "f95", "f03"]
roots = ["fpm.toml"]
comment-token = "!"
indent = { tab-width = 4, unit = " "}
language-server = { command = "fortls", args = ["--lowercase_intrinsics"] }
language-servers = [ "fortls" ]
[[grammar]]
name = "fortran"
@ -1880,7 +1935,7 @@ file-types = ["dot"]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "dot-language-server", args = ["--stdio"] }
language-servers = [ "dot-language-server" ]
[[grammar]]
name = "dot"
@ -1894,7 +1949,7 @@ file-types = ["cue"]
roots = ["cue.mod"]
auto-format = true
comment-token = "//"
language-server = { command = "cuelsp" }
language-servers = [ "cuelsp" ]
indent = { tab-width = 4, unit = "\t" }
formatter = { command = "cue", args = ["fmt", "-"] }
@ -1910,7 +1965,7 @@ file-types = ["slint"]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "slint-lsp", args = [] }
language-servers = [ "slint-lsp" ]
[[grammar]]
name = "slint"
@ -1962,7 +2017,7 @@ file-types = ["pas", "pp", "inc", "lpr", "lfm"]
roots = []
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "pasls", args = [] }
language-servers = [ "pasls" ]
[[grammar]]
name = "pascal"
@ -1987,7 +2042,7 @@ file-types = ["libsonnet", "jsonnet"]
roots = ["jsonnetfile.json"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] }
language-servers = [ "jsonnet-language-server" ]
[[grammar]]
name = "jsonnet"
@ -2013,7 +2068,7 @@ file-types = ["bass"]
roots = []
comment-token = ";"
indent = { tab-width = 2, unit = " " }
language-server = { command = "bass", args = ["--lsp"] }
language-servers = [ "bass" ]
[[grammar]]
name = "bass"
@ -2049,7 +2104,7 @@ roots = []
comment-token = "//"
injection-regex = "d"
indent = { tab-width = 4, unit = " "}
language-server = { command = "serve-d" }
language-servers = [ "serve-d" ]
formatter = { command = "dfmt" }
[[grammar]]
@ -2173,7 +2228,7 @@ roots = []
auto-format = true
comment-token = "//"
indent = { tab-width = 2, unit = " "}
language-server = { command = "bicep-langserver" }
language-servers = [ "bicep-langserver" ]
[[grammar]]
name = "bicep"
@ -2184,7 +2239,7 @@ name = "qml"
scope = "source.qml"
file-types = ["qml"]
roots = []
language-server = { command = "qmlls" }
language-servers = [ "qmlls" ]
indent = { tab-width = 4, unit = " " }
grammar = "qmljs"
@ -2239,7 +2294,7 @@ file-types = ["dhall"]
roots = []
comment-token = "--"
indent = { tab-width = 2, unit = " " }
language-server = { command = "dhall-lsp-server" }
language-servers = [ "dhall-lsp-server" ]
formatter = { command = "dhall" , args = ["format"] }
[[grammar]]
@ -2401,7 +2456,7 @@ file-types = ["smithy"]
roots = ["smithy-build.json"]
comment-token = "//"
indent = { tab-width = 4, unit = " " }
language-server = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] }
language-servers = [ "cs" ]
[[grammar]]
name = "smithy"
@ -2413,7 +2468,7 @@ scope = "source.vhdl"
file-types = ["vhd", "vhdl"]
roots = []
comment-token = "--"
language-server = { command = "vhdl_ls", args = [] }
language-servers = [ "vhdl_ls" ]
indent = { tab-width = 2, unit = " " }
injection-regex = "vhdl"
@ -2429,7 +2484,7 @@ injection-regex = "rego"
file-types = ["rego"]
auto-format = true
comment-token = "#"
language-server = { command = "regols" }
language-servers = [ "regols" ]
grammar = "rego"
[[grammar]]
@ -2445,7 +2500,7 @@ shebangs = []
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "nimlangserver" }
language-servers = [ "nimlangserver" ]
[language.auto-pairs]
'(' = ')'
@ -2485,7 +2540,7 @@ name = "markdoc"
scope = "text.markdoc"
roots = []
file-types = ["mdoc"]
language-server = { command = "markdoc-ls", args = ["--stdio"] }
language-servers = [ "markdoc-ls" ]
[[grammar]]
name = "markdoc"
@ -2498,7 +2553,7 @@ injection-regex = "(cl|opencl)"
file-types = ["cl"]
roots = []
comment-token = "//"
language-server = { command = "clangd" }
language-servers = [ "clangd" ]
[[grammar]]
name = "opencl"

View File

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