From 71551d395b4e47804df2d8ecea99e34dbbf16157 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 23 May 2022 18:10:48 +0200 Subject: [PATCH 01/41] Adds support for multiple language servers per language. Language Servers are now configured in a separate table in `languages.toml`: ```toml [langauge-server.mylang-lsp] command = "mylang-lsp" args = ["--stdio"] config = { provideFormatter = true } [language-server.efm-lsp-prettier] command = "efm-langserver" [language-server.efm-lsp-prettier.config] documentFormatting = true languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] } ``` The language server for a language is configured like this (`typescript-language-server` is configured by default): ```toml [[language]] name = "typescript" language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ] ``` or equivalent: ```toml [[language]] name = "typescript" language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ] ``` Each requested LSP feature is priorized in the order of the `language-servers` array. For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`). If no `except-features` or `only-features` is given all features for the language server are enabled, as long as the language server supports these. If it doesn't the next language server which supports the feature is tried. The list of supported features are: - `format` - `goto-definition` - `goto-declaration` - `goto-type-definition` - `goto-reference` - `goto-implementation` - `signature-help` - `hover` - `document-highlight` - `completion` - `code-action` - `workspace-command` - `document-symbols` - `workspace-symbols` - `diagnostics` - `rename-symbol` - `inlay-hints` Another side-effect/difference that comes with this PR, is that only one language server instance is started if different languages use the same language server. --- book/src/generated/typable-cmd.md | 2 +- book/src/guides/adding_languages.md | 1 + book/src/languages.md | 104 +++- helix-core/src/diagnostic.rs | 1 + helix-core/src/syntax.rs | 113 +++- helix-lsp/src/client.rs | 12 +- helix-lsp/src/lib.rs | 202 +++---- helix-lsp/src/transport.rs | 63 ++- helix-term/src/application.rs | 126 +++-- helix-term/src/commands.rs | 267 +++++---- helix-term/src/commands/lsp.rs | 803 +++++++++++++++------------- helix-term/src/commands/typed.rs | 106 ++-- helix-term/src/health.rs | 27 +- helix-term/src/ui/completion.rs | 85 ++- helix-term/src/ui/editor.rs | 17 +- helix-term/src/ui/mod.rs | 21 +- helix-term/src/ui/statusline.rs | 12 +- helix-view/src/document.rs | 145 +++-- helix-view/src/editor.rs | 64 ++- helix-view/src/gutter.rs | 2 +- languages.toml | 425 ++++++++------- xtask/src/docgen.rs | 11 +- 22 files changed, 1553 insertions(+), 1056 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index a775c655..0c377b3b 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -50,7 +50,7 @@ | `:reload-all` | Discard changes and reload all documents from the source files. | | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | -| `:lsp-restart` | Restarts the Language Server that is in use by the current doc | +| `:lsp-restart` | Restarts the language servers used by the currently opened file | | `:lsp-stop` | Stops the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index b92af402..93ec013f 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -9,6 +9,7 @@ below. necessary configuration for the new language. For more information on language configuration, refer to the [language configuration section](../languages.md) of the documentation. + A new language server can be added by extending the `[language-server]` table in the same file. 2. If you are adding a new language or updating an existing language server configuration, run the command `cargo xtask docgen` to update the [Language Support](../lang-support.md) documentation. diff --git a/book/src/languages.md b/book/src/languages.md index fe4db141..3328c610 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -18,6 +18,9 @@ There are three possible locations for a `languages.toml` file: ```toml # in <config_dir>/helix/languages.toml +[language-server.mylang-lsp] +command = "mylang-lsp" + [[language]] name = "rust" auto-format = false @@ -41,8 +44,8 @@ injection-regex = "mylang" file-types = ["mylang", "myl"] comment-token = "#" indent = { tab-width = 2, unit = " " } -language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } } formatter = { command = "mylang-formatter" , args = ["--stdin"] } +language-servers = [ "mylang-lsp" ] ``` These configuration keys are available: @@ -50,6 +53,7 @@ These configuration keys are available: | Key | Description | | ---- | ----------- | | `name` | The name of the language | +| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id | | `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages | | `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | | `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. | @@ -59,7 +63,7 @@ These configuration keys are available: | `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) | | `comment-token` | The token to use as a comment-token | | `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) | -| `language-server` | The Language Server to run. See the Language Server configuration section below. | +| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) | | `config` | Language Server configuration | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | @@ -92,31 +96,97 @@ with the following priorities: replaced at runtime with the appropriate path separator for the operating system, so this rule would match against `.git\config` files on Windows. -### Language Server configuration +## Language Server configuration -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 priorized in the order of the `language-servers` array. +For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`). +If no `except-features` or `only-features` is given all features for the language server are enabled. +If a language server itself doesn't support a feature the next language server array entry will be tried (and so on). + +The list of supported features are: + +- `format` +- `goto-definition` +- `goto-declaration` +- `goto-type-definition` +- `goto-reference` +- `goto-implementation` +- `signature-help` +- `hover` +- `document-highlight` +- `completion` +- `code-action` +- `workspace-command` +- `document-symbols` +- `workspace-symbols` +- `diagnostics` +- `rename-symbol` +- `inlay-hints` + ## Tree-sitter grammar configuration The source for a language's tree-sitter grammar is specified in a `[[grammar]]` diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index 58ddb038..0b75d2a5 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -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>, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f36c985e..ff4bb6c2 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -17,7 +17,7 @@ use std::{ borrow::Cow, cell::RefCell, collections::{HashMap, VecDeque}, - fmt, + fmt::{self, Display}, hash::{Hash, Hasher}, mem::{replace, transmute}, path::{Path, PathBuf}, @@ -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 @@ impl Default for Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, // c-sharp, rust + pub language_id: String, // c-sharp, rust, tsx + #[serde(rename = "language-id")] + // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem + pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server pub scope: String, // source.rust pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc> #[serde(default)] @@ -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,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 - #[serde(skip_serializing_if = "Option::is_none")] - pub language_server: Option<LanguageServerConfiguration>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub language_servers: Vec<LanguageServerFeatureConfiguration>, #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option<IndentationConfiguration>, @@ -208,6 +211,68 @@ impl<'de> Deserialize<'de> for FileType { } } +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum LanguageServerFeature { + Format, + GotoDeclaration, + GotoDefinition, + GotoTypeDefinition, + GotoReference, + GotoImplementation, + // Goto, use bitflags, combining previous Goto members? + SignatureHelp, + Hover, + DocumentHighlight, + Completion, + CodeAction, + WorkspaceCommand, + DocumentSymbols, + WorkspaceSymbols, + // Symbols, use bitflags, see above? + Diagnostics, + RenameSymbol, + InlayHints, +} + +impl Display for LanguageServerFeature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LanguageServerFeature::Format => write!(f, "format"), + LanguageServerFeature::GotoDeclaration => write!(f, "goto-declaration"), + LanguageServerFeature::GotoDefinition => write!(f, "goto-definition"), + LanguageServerFeature::GotoTypeDefinition => write!(f, "goto-type-definition"), + LanguageServerFeature::GotoReference => write!(f, "goto-type-definition"), + LanguageServerFeature::GotoImplementation => write!(f, "goto-implementation"), + LanguageServerFeature::SignatureHelp => write!(f, "signature-help"), + LanguageServerFeature::Hover => write!(f, "hover"), + LanguageServerFeature::DocumentHighlight => write!(f, "document-highlight"), + LanguageServerFeature::Completion => write!(f, "completion"), + LanguageServerFeature::CodeAction => write!(f, "code-action"), + LanguageServerFeature::WorkspaceCommand => write!(f, "workspace-command"), + LanguageServerFeature::DocumentSymbols => write!(f, "document-symbols"), + LanguageServerFeature::WorkspaceSymbols => write!(f, "workspace-symbols"), + LanguageServerFeature::Diagnostics => write!(f, "diagnostics"), + LanguageServerFeature::RenameSymbol => write!(f, "rename-symbol"), + LanguageServerFeature::InlayHints => write!(f, "inlay-hints"), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] +pub enum LanguageServerFeatureConfiguration { + #[serde(rename_all = "kebab-case")] + Features { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + only_features: Vec<LanguageServerFeature>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + except_features: Vec<LanguageServerFeature>, + name: String, + }, + Simple(String), +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -217,9 +282,10 @@ pub struct LanguageServerConfiguration { pub args: Vec<String>, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub environment: HashMap<String, String>, + #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] + pub config: Option<serde_json::Value>, #[serde(default = "default_timeout")] pub timeout: u64, - pub language_id: Option<String>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -584,6 +650,15 @@ pub struct SoftWrap { pub wrap_at_text_width: Option<bool>, } +impl LanguageServerFeatureConfiguration { + pub fn name(&self) -> &String { + match self { + LanguageServerFeatureConfiguration::Simple(name) => name, + LanguageServerFeatureConfiguration::Features { name, .. } => name, + } + } +} + // Expose loader as Lazy<> global since it's always static? #[derive(Debug)] @@ -594,6 +669,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 +678,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 +803,10 @@ impl Loader { self.language_configs.iter() } + pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> { + &self.language_server_configs + } + pub fn set_scopes(&self, scopes: Vec<String>) { self.scopes.store(Arc::new(scopes)); @@ -2370,7 +2452,10 @@ mod test { "#, ); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(Configuration { + language: vec![], + language_server: HashMap::new(), + }); let language = get_language("rust").unwrap(); let query = Query::new(language, query_str).unwrap(); @@ -2429,7 +2514,10 @@ mod test { .map(String::from) .collect(); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(Configuration { + language: vec![], + language_server: HashMap::new(), + }); let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new( @@ -2532,7 +2620,10 @@ mod test { ) { let source = Rope::from_str(source); - let loader = Loader::new(Configuration { language: vec![] }); + let loader = Loader::new(Configuration { + language: vec![], + language_server: HashMap::new(), + }); let language = get_language(language_name).unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap(); diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 840e7382..c0f3adb8 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -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 @@ impl Client { 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 @@ impl Client { 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 @@ impl Client { 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 @@ impl Client { let client = Self { id, + name, _process: process, server_tx, request_counter: AtomicU64::new(0), @@ -240,6 +242,10 @@ impl Client { Ok((client, server_rx, initialize_notify)) } + pub fn name(&self) -> &String { + &self.name + } + pub fn id(&self) -> usize { self.id } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 31ee1d75..12e63255 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -17,19 +17,16 @@ use helix_core::{ 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; +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 @@ impl 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,15 +641,43 @@ impl Registry { 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"))?; + self.counter += 1; + let id = self.counter; + 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) } pub fn restart( @@ -666,48 +686,46 @@ impl Registry { 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(|config| { + let name = config.name().clone(); - let scope = language_config.scope.clone(); + #[allow(clippy::map_entry)] + 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, vec![client.clone()]).unwrap(); - 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); + // TODO what if there are different language servers for different workspaces, + // I think the language servers will be stopped without being restarted, which is not intended + for old_client in old_clients { + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } - 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 +739,35 @@ impl Registry { 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<Vec<Arc<Client>>> { + language_config + .language_servers + .iter() + .map(|features| { + let name = features.name(); + if let Some(clients) = self.inner.get_mut(name) { + 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(client.clone()); + } + } + let client = self.start_client( + name.clone(), + language_config, + doc_path, + root_dirs, + enable_snippets, + )?; + let clients = self.inner.entry(features.name().clone()).or_default(); + clients.push(client.clone()); + Ok(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 @@ impl LspProgressMap { } } -struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>); +struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>); /// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that /// 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: diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs index 3e3e06ee..8c38c177 100644 --- a/helix-lsp/src/transport.rs +++ b/helix-lsp/src/transport.rs @@ -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 @@ impl Transport { server_stdin: BufWriter<ChildStdin>, server_stderr: BufReader<ChildStderr>, id: usize, + name: String, ) -> ( UnboundedReceiver<(usize, jsonrpc::Call)>, UnboundedSender<Payload>, @@ -58,6 +60,7 @@ impl Transport { let transport = Self { id, + name, pending_requests: Mutex::new(HashMap::default()), }; @@ -83,6 +86,7 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { &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 @@ impl Transport { 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 @@ impl Transport { ) { 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 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { 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 @@ impl Transport { match transport.send_payload_to_server(&mut server_stdin, msg).await { Ok(_) => {} Err(err) => { - error!("err: <- {:?}", err); + error!("{} err: <- {err:?}", transport.name); } } } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b54d6835..45f99e48 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -30,6 +30,7 @@ use crate::{ use log::{debug, error, warn}; use std::{ + collections::btree_map::Entry, io::{stdin, stdout}, path::Path, sync::Arc, @@ -564,7 +565,7 @@ impl Application { 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 @@ impl Application { ) { use helix_lsp::{Call, MethodCall, Notification}; + macro_rules! 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; + } + } + }; + } + match call { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { let notification = match Notification::parse(&method, params) { @@ -677,14 +690,7 @@ impl Application { 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 @@ -694,7 +700,7 @@ impl Application { } let docs = self.editor.documents().filter(|doc| { - doc.language_server().map(|server| server.id()) == Some(server_id) + doc.language_servers().iter().any(|l| l.id() == server_id) }); // trigger textDocument/didOpen for docs that are already open @@ -723,6 +729,7 @@ impl Application { 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 +752,11 @@ impl Application { 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 +764,9 @@ impl Application { 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 +805,19 @@ impl Application { 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,11 +833,12 @@ impl Application { 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. @@ -842,13 +846,26 @@ impl Application { params .diagnostics .sort_unstable_by_key(|d| (d.severity, d.range.start)); + let diagnostics = params + .diagnostics + .into_iter() + .map(|d| (d, server_id, offset_encoding)) + .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.extend(diagnostics); + } + Entry::Vacant(v) => { + v.insert(diagnostics); + } + }; } Notification::ShowMessage(params) => { log::warn!("unhandled window/showMessage: {:?}", params); @@ -950,10 +967,12 @@ impl Application { .editor .documents_mut() .filter_map(|doc| { - if doc.language_server().map(|server| server.id()) - == Some(server_id) + if doc + .language_servers() + .iter() + .any(|server| server.id() == server_id) { - doc.set_diagnostics(Vec::new()); + doc.clear_diagnostics(server_id); doc.url() } else { None @@ -1029,28 +1048,15 @@ impl Application { })) } 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()?, - }; + .filter_map(|item| { + let mut config = language_server.config()?; if let Some(section) = item.section.as_ref() { for part in section.split('.') { config = config.get(part)?; @@ -1074,15 +1080,7 @@ impl Application { } }; - 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), } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5a844e35..c7d28e19 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -23,6 +23,7 @@ use helix_core::{ regex::{self, Regex, RegexBuilder}, search::{self, CharMatcher}, selection, shellwords, surround, + syntax::LanguageServerFeature, text_annotations::TextAnnotations, textobject, tree_sitter::Node, @@ -54,13 +55,13 @@ use crate::{ 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,12 @@ fn goto_prev_diag(cx: &mut Context) { .cursor(doc.text().slice(..)); let diag = doc - .diagnostics() - .iter() + .shown_diagnostics() + .collect::<Vec<_>>() + .into_iter() .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,60 +3235,72 @@ pub mod insert { 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) + .iter() + .any(|ls| { + let capabilities = ls.capabilities(); - let capabilities = language_server.capabilities(); + // TODO: what if trigger is multiple chars long + matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), + .. + }) if triggers.iter().any(|trigger| trigger.contains(ch))) + }); - 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); } } fn signature_help(cx: &mut Context, ch: char) { + use futures_util::FutureExt; 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, - }; + let (view, doc) = current!(cx.editor); + // lsp doesn't tell us when to close the signature help, so we request + // the help information again after common close triggers which should + // return None, which in turn closes the popup. + let close_triggers = &[')', ';', '.']; + // TODO support multiple language servers (not just the first that is found) + let future = doc + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .iter() + .find_map(|ls| { + let capabilities = ls.capabilities(); - let capabilities = language_server.capabilities(); + match capabilities { + lsp::ServerCapabilities { + signature_help_provider: + Some(lsp::SignatureHelpOptions { + trigger_characters: Some(triggers), + // TODO: retrigger_characters + .. + }), + .. + } if triggers.iter().any(|trigger| trigger.contains(ch)) + || close_triggers.contains(&ch) => + { + let pos = doc.position(view.id, ls.offset_encoding()); + ls.text_document_signature_help(doc.identifier(), pos, None) + } + _ if close_triggers.contains(&ch) => ls.text_document_signature_help( + doc.identifier(), + doc.position(view.id, ls.offset_encoding()), + None, + ), + // TODO: what if trigger is multiple chars long + _ => None, + } + }); - if let lsp::ServerCapabilities { - signature_help_provider: - Some(lsp::SignatureHelpOptions { - trigger_characters: Some(triggers), - // TODO: retrigger_characters - .. - }), - .. - } = capabilities - { - // TODO: what if trigger is multiple chars long - let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); - // lsp doesn't tell us when to close the signature help, so we request - // the help information again after common close triggers which should - // return None, which in turn closes the popup. - let close_triggers = &[')', ';', '.']; - - if is_trigger || close_triggers.contains(&ch) { - super::signature_help_impl(cx, SignatureHelpInvoked::Automatic); - } + if let Some(future) = future { + super::signature_help_impl_with_future( + cx, + future.boxed(), + SignatureHelpInvoked::Automatic, + ) } } @@ -3301,7 +3314,7 @@ pub mod insert { Some(transaction) } - use helix_core::auto_pairs; + use helix_core::{auto_pairs, syntax::LanguageServerFeature}; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current_ref!(cx.editor); @@ -4046,55 +4059,55 @@ 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: handle fails - // TODO: concurrent map over all ranges + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::Format) + .iter() + .find_map(|language_server| { + 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(); - let range = ranges[0]; + // TODO: handle fails + // TODO: concurrent map over all ranges - let request = match language_server.text_document_range_formatting( - doc.identifier(), - range, - lsp::FormattingOptions::default(), - None, - ) { - Some(future) => future, + let range = ranges[0]; + + let future = language_server.text_document_range_formatting( + doc.identifier(), + range, + lsp::FormattingOptions::default(), + None, + )?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support range formatting"); + .set_error("No language server supports range formatting"); return; } }; - let edits = tokio::task::block_in_place(|| helix_lsp::block_on(request)).unwrap_or_default(); + let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default(); - let transaction = helix_lsp::util::generate_transaction_from_edits( - 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 +4244,45 @@ 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 futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::Completion) + .iter() + // TODO this should probably already been filtered in something like "language_servers_with_feature" + .filter_map(|language_server| { + let language_server_id = language_server.id(); + let offset_encoding = language_server.offset_encoding(); + let pos = pos_to_lsp_pos(doc.text(), cursor, helix_lsp::OffsetEncoding::Utf8); + let completion_request = language_server.completion(doc.identifier(), pos, None)?; - let future = match language_server.completion(doc.identifier(), pos, None) { - Some(future) => future, - None => return, - }; + Some(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, + offset_encoding, + resolved: false, + }) + .collect(); + + anyhow::Ok(items) + }) + }) + .collect(); // setup a channel that allows the request to be canceled let (tx, rx) = oneshot::channel(); @@ -4254,12 +4291,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 +4338,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 +4351,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 +4361,6 @@ pub fn completion(cx: &mut Context) { editor, savepoint, items, - offset_encoding, start_offset, trigger_offset, size, @@ -4340,8 +4374,9 @@ pub fn completion(cx: &mut Context) { { compositor.remove(SignatureHelp::ID); } - }, - ); + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }); } // comments @@ -5141,7 +5176,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(), diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 0ad6fb7e..efef1211 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,4 +1,4 @@ -use futures_util::FutureExt; +use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt}; use helix_lsp::{ block_on, lsp::{ @@ -8,6 +8,8 @@ use helix_lsp::{ util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; +use serde_json::Value; +use tokio_stream::StreamExt; use tui::{ text::{Span, Spans}, widgets::Row, @@ -15,7 +17,9 @@ use tui::{ 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 helix_view::{ use crate::{ compositor::{self, Compositor}, + job::Callback, ui::{ self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, Popup, PromptEvent, @@ -35,24 +40,6 @@ use std::{ cmp::Ordering, collections::BTreeMap, 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. -#[macro_export] -macro_rules! language_server { - ($editor:expr, $doc:expr) => { - match $doc.language_server() { - Some(language_server) => language_server, - None => { - $editor.set_status("Language server not active for current buffer"); - return; - } - } - }; -} - impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; @@ -87,20 +74,30 @@ impl ui::menu::Item for lsp::Location { } } -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 +113,7 @@ struct DiagnosticStyles { struct PickerDiagnostic { url: lsp::Url, diag: lsp::Diagnostic, + offset_encoding: OffsetEncoding, } impl ui::menu::Item for PickerDiagnostic { @@ -211,21 +209,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 +241,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 +249,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 +262,9 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>, + diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>, current_path: Option<lsp::Url>, format: DiagnosticsFormat, - offset_encoding: OffsetEncoding, ) -> FilePicker<PickerDiagnostic> { // TODO: drop current_path comparison and instead use workspace: bool flag? @@ -277,10 +272,11 @@ fn diag_picker( let mut flat_diag = Vec::new(); for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); - for diag in diags { + for (diag, _, offset_encoding) in diags { flat_diag.push(PickerDiagnostic { url: url.clone(), diag, + offset_encoding, }); } } @@ -295,7 +291,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 +308,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 +325,149 @@ 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 future = match language_server.document_symbols(doc.identifier()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support document symbols"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option<lsp::DocumentSymbolResponse>| { - if let Some(symbols) = response { - // 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::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) - } - flat_symbols + let mut futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) + .iter() + .filter_map(|ls| { + let request = ls.document_symbols(doc.identifier())?; + Some((request, ls.offset_encoding(), doc.identifier())) + }) + .map(|(request, offset_encoding, doc_id)| 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 + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(), + lsp::DocumentSymbolResponse::Nested(symbols) => { + let mut flat_symbols = Vec::new(); + for symbol in symbols { + nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) } - }; + flat_symbols + } + }; + Ok(symbols) + }) + .collect(); + let current_url = doc.url(); - let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlaid(picker))) + if futures.is_empty() { + cx.editor + .set_error("No Language server does support 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| { + if symbols.is_empty() { + editor.set_error("No symbols available"); + return; } - }, - ) + 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 futures: FuturesUnordered<_> = doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) + .iter() + .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) + .map(|(request, 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 Language server does support 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 +479,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 +486,28 @@ 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, + offset_encoding: OffsetEncoding, + 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 +574,40 @@ 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 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) + .iter() + // 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, offset_encoding, language_server_id)) + }) + .map(|(request, offset_encoding, 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 +619,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 +654,48 @@ 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, + offset_encoding, + language_server_id: ls_id, + }) + .collect()) + }) + .collect(); + + if futures.is_empty() { + cx.editor + .set_error("No Language server does support 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 offset_encoding = action.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 +707,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 +716,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 +729,14 @@ impl ui::menu::Item for lsp::Command { } } -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_servers + .get_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,18 +1028,22 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo } } +// TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity... pub fn goto_declaration(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_declaration(doc.identifier(), pos, None) { - Some(future) => future, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::GotoDeclaration) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.goto_declaration(doc.identifier(), pos, None)?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support goto-declaration"); + .set_error("No language server supports goto-declaration"); return; } }; @@ -1004,16 +1059,19 @@ pub fn goto_declaration(cx: &mut Context) { 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, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::GotoDefinition) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.goto_definition(doc.identifier(), pos, None)?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support goto-definition"); + .set_error("No language server supports goto-definition"); return; } }; @@ -1029,16 +1087,19 @@ pub fn goto_definition(cx: &mut Context) { 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, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.goto_type_definition(doc.identifier(), pos, None)?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support goto-type-definition"); + .set_error("No language server supports goto-type-definition"); return; } }; @@ -1054,16 +1115,19 @@ pub fn goto_type_definition(cx: &mut Context) { 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, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::GotoImplementation) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.goto_implementation(doc.identifier(), pos, None)?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support goto-implementation"); + .set_error("no language server supports goto-implementation"); return; } }; @@ -1080,21 +1144,24 @@ pub fn goto_implementation(cx: &mut Context) { 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); - 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, + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::GotoReference) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.goto_reference( + doc.identifier(), + pos, + config.lsp.goto_reference_include_declaration, + None, + )?; + Some((future, offset_encoding)) + }) { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("Language server does not support goto-reference"); + .set_error("language server supports goto-reference"); return; } }; @@ -1108,7 +1175,7 @@ pub fn goto_reference(cx: &mut Context) { ); } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Clone, Copy)] pub enum SignatureHelpInvoked { Manual, Automatic, @@ -1120,35 +1187,34 @@ 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, + // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it + let future = match doc + .language_servers_with_feature(LanguageServerFeature::SignatureHelp) + .iter() + .find_map(|language_server| { + let pos = doc.position(view.id, language_server.offset_encoding()); + language_server.text_document_signature_help(doc.identifier(), pos, None) + }) { + Some(future) => future.boxed(), None => { // Do not show the message if signature help was invoked // automatically on backspace, trigger characters, etc. - if was_manually_invoked { + if invoked == SignatureHelpInvoked::Manual { 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"); + .set_error("No language server supports signature-help"); } return; } }; + signature_help_impl_with_future(cx, future, 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 +1222,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 +1231,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,18 +1321,20 @@ 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: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier + let request = doc + .language_servers_with_feature(LanguageServerFeature::Hover) + .iter() + .find_map(|language_server| { + let pos = doc.position(view.id, language_server.offset_encoding()); + language_server.text_document_hover(doc.identifier(), pos, None) + }); - let pos = doc.position(view.id, offset_encoding); - - let future = match language_server.text_document_hover(doc.identifier(), pos, None) { + let future = match request { Some(future) => future, None => { - cx.editor - .set_error("Language server does not support hover"); + cx.editor.set_error("No language server supports hover"); return; } }; @@ -1349,7 +1417,11 @@ pub fn rename_symbol(cx: &mut Context) { } } - 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,27 +1430,36 @@ pub fn rename_symbol(cx: &mut Context) { if event != PromptEvent::Validate { return; } - 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.rename_symbol(doc.identifier(), pos, input.to_string()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support symbol renaming"); - return; + let request = doc + .language_servers_with_feature(LanguageServerFeature::RenameSymbol) + .iter() + .find_map(|language_server| { + if let Some(language_server_id) = language_server_id { + if language_server.id() != language_server_id { + return None; + } } - }; - match block_on(future) { - Ok(edits) => { - let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); + 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(), + )?; + Some((future, offset_encoding)) + }); + + if let Some((future, offset_encoding)) = request { + match block_on(future) { + Ok(edits) => { + let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); + } + Err(err) => cx.editor.set_error(err.to_string()), } - Err(err) => cx.editor.set_error(err.to_string()), + } else { + cx.editor + .set_error("No language server supports symbol renaming"); } }, ) @@ -1388,20 +1469,20 @@ pub fn rename_symbol(cx: &mut Context) { } let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); - if !language_server.supports_rename() { - cx.editor - .set_error("Language server does not support symbol renaming"); - return; - } + let prepare_rename_request = doc + .language_servers_with_feature(LanguageServerFeature::RenameSymbol) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = language_server.prepare_rename(doc.identifier(), pos)?; + Some((future, offset_encoding, language_server.id())) + }); - let pos = doc.position(view.id, offset_encoding); - - match language_server.prepare_rename(doc.identifier(), pos) { + match prepare_rename_request { // Language server supports textDocument/prepareRename, use it. - Some(future) => cx.callback( + Some((future, offset_encoding, ls_id)) => cx.callback( future, move |editor, compositor, response: Option<lsp::PrepareRenameResponse>| { let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response) @@ -1413,7 +1494,7 @@ pub fn rename_symbol(cx: &mut Context) { } }; - let prompt = create_rename_prompt(editor, prefill); + let prompt = create_rename_prompt(editor, prefill, Some(ls_id)); compositor.push(prompt); }, @@ -1423,7 +1504,7 @@ pub fn rename_symbol(cx: &mut Context) { None => { let prefill = get_prefill_from_word_boundary(cx.editor); - let prompt = create_rename_prompt(cx.editor, prefill); + let prompt = create_rename_prompt(cx.editor, prefill, None); cx.push_layer(prompt); } @@ -1432,17 +1513,20 @@ pub fn rename_symbol(cx: &mut Context) { 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 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) - { + let (future, offset_encoding) = match doc + .language_servers_with_feature(LanguageServerFeature::DocumentHighlight) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = doc.position(view.id, offset_encoding); + let future = + language_server.text_document_document_highlight(doc.identifier(), pos, None)?; + Some((future, offset_encoding)) + }) { Some(future) => future, None => { cx.editor - .set_error("Language server does not support document highlight"); + .set_error("No language server supports document-highlight"); return; } }; @@ -1455,8 +1539,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 +1584,58 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let language_server = doc.language_server()?; - - let capabilities = language_server.capabilities(); - - 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); - - 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, + let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); + let language_server = language_servers.iter().find(|language_server| { + matches!( + language_server.capabilities().inlay_hint_provider, + Some( + lsp::OneOf::Left(true) + | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)) ) - } - _ => return None, + ) + })?; + + 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); + + 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 +1649,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, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 81a24059..b78de772 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1329,23 +1329,20 @@ 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, + let doc = doc!(cx.editor); + let language_servers = + doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); + let (language_server_id, options) = match language_servers.iter().find_map(|ls| { + ls.capabilities() + .execute_command_provider + .as_ref() + .map(|options| (ls.id(), options)) + }) { + Some(id_options) => id_options, None => { - cx.editor - .set_status("Language server not active for current buffer"); - 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"); + cx.editor.set_status( + "No active language servers for this document support workspace commands", + ); return Ok(()); } }; @@ -1362,8 +1359,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 +1373,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, @@ -1426,7 +1424,7 @@ fn lsp_restart( .collect(); for document_id in document_ids_to_refresh { - cx.editor.refresh_language_server(document_id); + cx.editor.refresh_language_servers(document_id); } Ok(()) @@ -1443,21 +1441,63 @@ fn lsp_stop( let doc = doc!(cx.editor); - let ls_id = doc - .language_server() - .map(|ls| ls.id()) - .context("LSP not running for the current document")?; + // TODO this stops language servers which may be used in another doc/language type that uses the same language servers + // I'm not sure if this is really what we want + let ls_shutdown_names = doc + .language_servers() + .iter() + .map(|ls| ls.name()) + .collect::<Vec<_>>(); - let config = doc - .language_config() - .context("LSP not defined for the current document")?; - cx.editor.language_servers.stop(config); + for ls_name in &ls_shutdown_names { + cx.editor.language_servers.stop(ls_name); + } - 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()); + let doc_ids_active_clients: Vec<_> = cx + .editor + .documents() + .filter_map(|doc| { + let doc_active_ls_ids: Vec<_> = doc + .language_servers() + .iter() + .filter(|ls| !ls_shutdown_names.contains(&ls.name())) + .map(|ls| ls.id()) + .collect(); + + let active_clients: Vec<_> = cx + .editor + .language_servers + .iter_clients() + .filter(|client| doc_active_ls_ids.contains(&client.id())) + .map(Clone::clone) + .collect(); + + if active_clients.len() != doc.language_servers().len() { + Some((doc.id(), active_clients)) + } else { + None + } + }) + .collect(); + + for (doc_id, active_clients) in doc_ids_active_clients { + let doc = cx.editor.documents.get_mut(&doc_id).unwrap(); + + let stopped_clients: Vec<_> = doc + .language_servers() + .iter() + .filter(|ls| { + !active_clients + .iter() + .any(|active_ls| active_ls.id() == ls.id()) + }) + .map(|ls| ls.id()) + .collect(); // is necessary because of borrow-checking + + for client_id in stopped_clients { + doc.clear_diagnostics(client_id) } + doc.set_language_servers(active_clients); } Ok(()) @@ -1850,7 +1890,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,7 +2628,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ 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(), }, diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 480c2c67..031f982c 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -2,7 +2,10 @@ use crossterm::{ style::{Color, Print, Stylize}, tty::IsTty, }; -use helix_core::config::{default_syntax_loader, user_syntax_loader}; +use helix_core::{ + config::{default_syntax_loader, user_syntax_loader}, + syntax::LanguageServerFeatureConfiguration, +}; use helix_loader::grammar::load_runtime_file; use helix_view::clipboard::get_clipboard_provider; use std::io::Write; @@ -192,10 +195,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(|lsp| { + syn_loader_conf + .language_server + .get(lsp.name()) + .map(|config| config.command.clone()) + }); check_binary(lsp); let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string()); @@ -264,11 +271,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> { } }; + // 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(|lsp| { + syn_loader_conf + .language_server + .get(lsp.name()) + .map(|config| config.command.clone()) + }), )?; probe_protocol( diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c5c40580..859403a7 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,7 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor}; 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 @@ impl menu::Item for CompletionItem { || 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,16 @@ impl menu::Item for CompletionItem { } 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 offset_encoding: OffsetEncoding, + pub resolved: bool, } /// Wraps a Menu. @@ -109,21 +107,13 @@ 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| { @@ -131,7 +121,6 @@ impl Completion { doc: &Document, view_id: ViewId, item: &CompletionItem, - offset_encoding: helix_lsp::OffsetEncoding, trigger_offset: usize, include_placeholder: bool, replace_mode: bool, @@ -154,6 +143,8 @@ impl Completion { } }; + let offset_encoding = item.offset_encoding; + let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ return Transaction::new(doc.text()); }; @@ -247,15 +238,8 @@ impl Completion { // always present here let item = item.unwrap(); - let transaction = item_to_transaction( - doc, - view.id, - item, - offset_encoding, - trigger_offset, - true, - replace_mode, - ); + let transaction = + item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode); doc.apply_temporary(&transaction, view.id); } PromptEvent::Validate => { @@ -267,10 +251,15 @@ impl Completion { // always present here let mut item = item.unwrap().clone(); + 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; } @@ -281,7 +270,6 @@ impl Completion { doc, view.id, &item, - offset_encoding, trigger_offset, false, replace_mode, @@ -299,7 +287,7 @@ impl Completion { let transaction = util::generate_transaction_from_edits( doc.text(), additional_edits, - offset_encoding, // TODO: should probably transcode in Client + item.offset_encoding, // TODO: should probably transcode in Client ); doc.apply(&transaction, view.id); } @@ -323,10 +311,17 @@ impl Completion { } 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 completion_resolve_provider = language_server + .capabilities() + .completion_provider + .as_ref()? + .resolve_provider; + if completion_resolve_provider != Some(true) { + return None; + } let future = language_server.resolve_completion_item(completion_item)?; let response = helix_lsp::block_on(future); @@ -397,8 +392,11 @@ impl Completion { Some(item) if !item.resolved => item.clone(), _ => return false, }; - - let language_server = match doc!(cx.editor).language_server() { + let language_server = match cx + .editor + .language_servers + .get_by_id(current_item.language_server_id) + { Some(language_server) => language_server, None => return false, }; @@ -422,13 +420,14 @@ impl Completion { .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, + offset_encoding: current_item.offset_encoding, + resolved: true, + }; + + completion.replace_item(current_item, resolved_item); } }, ); diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f0989fa8..43b5d1af 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc}; use tui::{buffer::Buffer as Surface, text::Span}; -use super::statusline; +use super::{completion::CompletionItem, statusline}; use super::{document::LineDecoration, lsp::SignatureHelp}; pub struct EditorView { @@ -650,7 +650,7 @@ impl EditorView { .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 @@ impl EditorView { &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 diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 3e9a14b0..118836c0 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -17,7 +17,7 @@ mod text; 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,17 +394,13 @@ pub mod completers { 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, + let language_servers = + doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); + let options = match language_servers + .into_iter() + .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) + { + Some(id_options) => id_options, None => { return vec![]; } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 88786351..b10e8076 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -197,15 +197,16 @@ where ); } +// 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_servers = context.doc.language_servers(); write( context, - context - .doc - .language_server() + language_servers + .first() .and_then(|srv| { context .spinners @@ -225,8 +226,7 @@ where { 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 +266,7 @@ where .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, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index eb376567..734d76d1 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -6,7 +6,7 @@ use futures_util::FutureExt; 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, LanguageServerFeatureConfiguration}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -16,7 +16,7 @@ use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; use std::cell::Cell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; @@ -180,7 +180,7 @@ pub struct Document { pub(crate) modified_since_accessed: bool, diagnostics: Vec<Diagnostic>, - language_server: Option<Arc<helix_lsp::Client>>, + language_servers: Vec<Arc<helix_lsp::Client>>, diff_handle: Option<DiffHandle>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>, @@ -616,7 +616,7 @@ impl Document { last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, - language_server: None, + language_servers: Vec::new(), diff_handle: None, config, version_control_head: None, @@ -730,19 +730,24 @@ impl Document { return Some(formatting_future.boxed()); }; - let language_server = self.language_server()?; let text = self.text.clone(); - let offset_encoding = language_server.offset_encoding(); - - let request = language_server.text_document_formatting( - self.identifier(), - lsp::FormattingOptions { - tab_size: self.tab_width() as u32, - insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), - ..Default::default() - }, - None, - )?; + // finds first language server that supports formatting and then formats + let (offset_encoding, request) = self + .language_servers_with_feature(LanguageServerFeature::Format) + .iter() + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions { + tab_size: self.tab_width() as u32, + insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), + ..Default::default() + }, + None, + )?; + Some((offset_encoding, request)) + })?; let fut = async move { let edits = request.await.unwrap_or_else(|e| { @@ -797,13 +802,12 @@ impl Document { 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 +851,13 @@ impl Document { 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?; } @@ -1005,8 +1008,8 @@ impl Document { } /// Set the LSP. - pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) { - self.language_server = language_server; + pub fn set_language_servers(&mut self, language_servers: Vec<Arc<helix_lsp::Client>>) { + self.language_servers = language_servers; } /// Select text within the [`Document`]. @@ -1159,7 +1162,7 @@ impl Document { 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 +1418,13 @@ impl Document { .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 +1437,54 @@ impl Document { 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) + /// Language servers that have been initialized. + pub fn language_servers(&self) -> Vec<&helix_lsp::Client> { + self.language_servers + .iter() + .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None }) + .collect() + } + + // TODO filter also based on LSP capabilities? + pub fn language_servers_with_feature( + &self, + feature: LanguageServerFeature, + ) -> Vec<&helix_lsp::Client> { + let language_servers = self.language_servers(); + + let language_config = match self.language_config() { + Some(language_config) => language_config, + None => return Vec::new(), + }; + + // O(n^2) but since language_servers will be of very small length, + // I don't see the necessity to optimize + language_config + .language_servers + .iter() + .filter_map(|c| match c { + LanguageServerFeatureConfiguration::Simple(name) => language_servers + .iter() + .find(|ls| ls.name() == name) + .copied(), + LanguageServerFeatureConfiguration::Features { + only_features, + except_features, + name, + } => { + if (only_features.is_empty() || only_features.contains(&feature)) + && !except_features.contains(&feature) + { + language_servers + .iter() + .find(|ls| ls.name() == name) + .copied() + } else { + None + } + } + }) + .collect() } pub fn diff_handle(&self) -> Option<&DiffHandle> { @@ -1565,12 +1607,33 @@ impl Document { &self.diagnostics } - pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { - self.diagnostics = diagnostics; + pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> { + let ls_ids: HashSet<_> = self + .language_servers_with_feature(LanguageServerFeature::Diagnostics) + .iter() + .map(|ls| ls.id()) + .collect(); + self.diagnostics + .iter() + .filter(move |d| ls_ids.contains(&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 diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9546d460..5ca9aceb 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -48,7 +48,7 @@ use helix_core::{ }; use helix_core::{Position, Selection}; use helix_dap as dap; -use helix_lsp::lsp; +use helix_lsp::{lsp, OffsetEncoding}; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE @@ -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, OffsetEncoding)>>, pub diff_providers: DiffProviderRegistry, pub debugger: Option<dap::Client>, @@ -941,6 +941,7 @@ impl Editor { 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 @@ impl Editor { 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, @@ -1093,12 +1094,12 @@ impl Editor { } /// 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; } @@ -1109,42 +1110,49 @@ impl Editor { 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_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())); + if let Some(language_servers) = language_servers { + // only spawn new lang servers if the servers aren't the same + let doc_language_servers = doc.language_servers(); + let spawn_new_servers = language_servers.len() != doc_language_servers.len() + || language_servers + .iter() + .zip(doc_language_servers.iter()) + .any(|(l, dl)| l.id() != dl.id()); + if spawn_new_servers { + for doc_language_server in doc_language_servers { + tokio::spawn(doc_language_server.text_document_did_close(doc.identifier())); } let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); - // 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.version(), - doc.text(), - language_id, - )); + for language_server in &language_servers { + // TODO: this now races with on_init code if the init happens too quickly + tokio::spawn(language_server.text_document_did_open( + doc_url.clone(), + doc.version(), + doc.text(), + language_id.clone(), + )); + } - doc.set_language_server(Some(language_server)); + doc.set_language_servers(language_servers); } } Some(()) @@ -1337,10 +1345,10 @@ impl Editor { } 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 doc_id = self.new_document(doc); + let _ = self.launch_language_servers(doc_id); - id + doc_id }; self.switch(id, action); @@ -1368,7 +1376,7 @@ impl Editor { // 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())); } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 3ecae919..78f879c9 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -55,7 +55,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.shown_diagnostics().collect::<Vec<_>>(); Box::new( move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { diff --git a/languages.toml b/languages.toml index 6516cc37..d5f07dcd 100644 --- a/languages.toml +++ b/languages.toml @@ -1,6 +1,153 @@ # Language support configuration. # See the languages documentation: https://docs.helix-editor.com/master/languages.html +[language-server] + +taplo = { command = "taplo", args = ["lsp", "stdio"] } +elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } } +mint = { command = "mint", args = ["ls"] } +vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } } +clangd = { command = "clangd" } +crystalline = { command = "crystalline", args = ["--stdio"] } +omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] } +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 } } +pylsp = { command = "pylsp" } +nls = { command = "nls" } +nil = { command = "nil" } +solargraph = { command = "solargraph", args = ["stdio"] } +bash-language-server = { command = "bash-language-server", args = ["start"] } +intelephense = { command = "intelephense", args = ["--stdio"] } +texlab = { command = "texlab" } +lean = { command = "lean", args = [ "--server" ] } +julia = { command = "julia", timeout = 60, args = [ + "--startup-file=no", + "--history-file=no", + "--quiet", + "-e", + "using LanguageServer; runserver()", +] } +jdtls = { command = "jdtls" } +ocamllsp = { command = "ocamllsp" } + +svelteserver = { command = "svelteserver", args = ["--stdio"] } +vuels = { command = "vls" } +yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] } +haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] } +zls = { command = "zls" } +swipl = { command = "swipl", args = [ + "-g", "use_module(library(lsp_server))", + "-g", "lsp_server:main", + "-t", "halt", "--", "stdio" +] } +cmake-language-server = { command = "cmake-language-server" } +racket = { command = "racket", args = ["-l", "racket-langserver"] } +dart = { command = "dart", args = ["language-server", "--client-id=helix"] } +metals = { command = "metals", config = { "isHttpEnabled" = true } } +docker-langserver = { command = "docker-langserver", args = ["--stdio"] } +elm-language-server = { command = "elm-language-server" } +rescript-language-server = { command = "rescript-language-server", args = ["--stdio"] } +robotframework_ls = { command = "robotframework_ls" } +erlang-ls = { command = "erlang_ls" } +kotlin-language-server = { command = "kotlin-language-server" } +terraform-ls = { command = "terraform-ls", args = ["serve"] } +solc = { command = "solc", args = ["--lsp"] } +R = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] } +sourcekit-lsp = { command = "sourcekit-lsp" } +vala-language-server = { command = "vala-language-server" } +svlangserver = { command = "svlangserver", args = [] } +vlang-language-server = { command = "v", args = ["ls"] } +openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] } +perlnavigator = { command = "perlnavigator", args= ["--stdio"] } +prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] } +clojure-lsp = { command = "clojure-lsp" } +wgsl_analyzer = { command = "wgsl_analyzer" } +elvish = { command = "elvish", args = ["-lsp"] } +idris2-lsp = { command = "idris2-lsp" } +fortls = { command = "fortls", args = ["--lowercase_intrinsics"] } +gleam = { command = "gleam", args = ["lsp"] } +ols = { command = "ols", args = [] } +dot-language-server = { command = "dot-language-server", args = ["--stdio"] } +cuelsp = { command = "cuelsp" } +slint-lsp = { command = "slint-lsp", args = [] } +awk-language-server = { command = "awk-language-server" } +pasls = { command = "pasls", args = [] } +jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "--lint"] } +marksman = { command = "marksman", args = ["server"] } +markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] } +bass = { command = "bass", args = ["--lsp"] } +purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] } +serve-d = { command = "serve-d" } +bicep-langserver = { command = "bicep-langserver" } +qmlls = { command = "qmlls" } +cl-lsp = { command = "cl-lsp", args = [ "stdio" ] } +dhall-lsp-server = { command = "dhall-lsp-server" } +forc = { command = "forc", args = ["lsp"] } +cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] } +vhdl_ls = { command = "vhdl_ls", args = [] } +regols = { command = "regols" } +nimlangserver = { command = "nimlangserver" } + +[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.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.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 +156,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 +166,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 +204,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 +220,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 +234,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 +262,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 +291,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 +300,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 +315,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 +352,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 +390,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 +400,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 +433,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 +480,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 +494,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 +509,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 +520,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 +549,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 +577,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 +594,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 +608,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 +622,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 +638,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 +654,7 @@ file-types = ["ncl"] shebangs = [] roots = [] comment-token = "#" -language-server = { command = "nls" } +language-servers = [ "nls" ] indent = { tab-width = 2, unit = " " } [[grammar]] @@ -587,7 +669,7 @@ file-types = ["nix"] shebangs = [] roots = [] comment-token = "#" -language-server = { command = "nil" } +language-servers = [ "nil" ] indent = { tab-width = 2, unit = " " } [[grammar]] @@ -602,7 +684,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 +699,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 +713,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 +739,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 +753,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 +781,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 +795,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 +808,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 +849,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 +870,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 +893,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 +906,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 +919,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 +932,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 +946,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 +960,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 +974,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 +1012,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 +1034,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 +1074,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 +1088,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 +1099,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 +1125,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 +1188,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 +1214,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 +1228,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 +1242,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 +1282,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 +1358,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 +1384,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 +1400,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 +1421,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 +1431,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 +1446,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 +1475,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 +1489,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 +1513,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 +1528,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 +1542,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 +1552,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 +1599,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 +1670,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 +1735,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 +1786,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 +1801,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 +1840,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 +1854,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 +1868,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 +1892,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 +1908,7 @@ shebangs = [] roots = [] comment-token = "--" indent = { tab-width = 2, unit = " " } -language-server = { command = "idris2-lsp" } +language-servers = [ "idris2-lsp" ] [[language]] name = "fortran" @@ -1853,7 +1918,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 +1945,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 +1959,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 +1975,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 +2027,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 +2052,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 +2078,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 +2114,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 +2238,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 +2249,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 +2304,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 +2466,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 +2478,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 +2494,7 @@ injection-regex = "rego" file-types = ["rego"] auto-format = true comment-token = "#" -language-server = { command = "regols" } +language-servers = [ "regols" ] grammar = "rego" [[grammar]] @@ -2445,7 +2510,7 @@ shebangs = [] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } -language-server = { command = "nimlangserver" } +language-servers = [ "nimlangserver" ] [language.auto-pairs] '(' = ')' @@ -2485,7 +2550,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 +2563,7 @@ injection-regex = "(cl|opencl)" file-types = ["cl"] roots = [] comment-token = "//" -language-server = { command = "clangd" } +language-servers = [ "clangd" ] [[grammar]] name = "opencl" diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index 473882f3..3bd0592e 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -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)); From 7d4f7eb4bda6e507e253cb276ccce254fdd51575 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 27 Mar 2023 21:05:27 +0200 Subject: [PATCH 02/41] Fix 'WorkspaceConfiguration' request with empty configuration section strings --- helix-term/src/application.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 45f99e48..53fbe37d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1058,8 +1058,11 @@ impl Application { .filter_map(|item| { 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) From 74e21e1b250be884242596cbd7d98b098bb0fd0c Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 13 Mar 2023 15:01:11 +0100 Subject: [PATCH 03/41] Fix some lints/docgen hints --- book/src/generated/typable-cmd.md | 2 +- helix-term/src/health.rs | 5 +---- helix-view/src/editor.rs | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 0c377b3b..cc2e87ea 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -50,7 +50,7 @@ | `:reload-all` | Discard changes and reload all documents from the source files. | | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | -| `:lsp-restart` | Restarts the language servers used by the currently opened file | +| `:lsp-restart` | Restarts the language servers used by the current doc | | `:lsp-stop` | Stops the Language Server that is in use by the current doc | | `: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. | diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 031f982c..6b9f8517 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -2,10 +2,7 @@ use crossterm::{ style::{Color, Print, Stylize}, tty::IsTty, }; -use helix_core::{ - config::{default_syntax_loader, user_syntax_loader}, - syntax::LanguageServerFeatureConfiguration, -}; +use helix_core::config::{default_syntax_loader, user_syntax_loader}; use helix_loader::grammar::load_runtime_file; use helix_view::clipboard::get_clipboard_provider; use std::io::Write; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5ca9aceb..697d4459 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE From 05583f8cc9679306d3907c9d48a351417552bbec Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 16 Mar 2023 00:34:05 +0100 Subject: [PATCH 04/41] Fix hardcoded offset_encoding --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c7d28e19..27f289f0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4254,7 +4254,7 @@ pub fn completion(cx: &mut Context) { .filter_map(|language_server| { let language_server_id = language_server.id(); let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), cursor, helix_lsp::OffsetEncoding::Utf8); + let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); let completion_request = language_server.completion(doc.identifier(), pos, None)?; Some(async move { From 44b2b401907c501aca7e3f7340730406b8c3a1fb Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 16 Mar 2023 00:35:06 +0100 Subject: [PATCH 05/41] Fix issue with ltex-ls, filtering params is not what we want here --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 53fbe37d..e159cb83 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1055,7 +1055,7 @@ impl Application { let result: Vec<_> = params .items .iter() - .filter_map(|item| { + .map(|item| { let mut config = language_server.config()?; if let Some(section) = item.section.as_ref() { // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server') From b6c60beb2d01f95cf8a98e053e58546100db8619 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 16 Mar 2023 00:39:35 +0100 Subject: [PATCH 06/41] Remove unnecessary completion support check (likely an artifact) --- helix-term/src/ui/completion.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 859403a7..980bc8b6 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -314,15 +314,6 @@ impl Completion { language_server: &helix_lsp::Client, completion_item: lsp::CompletionItem, ) -> Option<lsp::CompletionItem> { - let completion_resolve_provider = language_server - .capabilities() - .completion_provider - .as_ref()? - .resolve_provider; - if completion_resolve_provider != Some(true) { - return None; - } - let future = language_server.resolve_completion_item(completion_item)?; let response = helix_lsp::block_on(future); match response { From 4da6d8ccc7ea4c31cbb31965ee0e6a25b7d403e9 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 16 Mar 2023 00:41:43 +0100 Subject: [PATCH 07/41] str instead of String --- helix-lsp/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index c0f3adb8..65c6954d 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -242,7 +242,7 @@ impl Client { Ok((client, server_rx, initialize_notify)) } - pub fn name(&self) -> &String { + pub fn name(&self) -> &str { &self.name } From f9b08656f41cbb9573ffb144f5dc2e24ea764ac9 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Fri, 17 Mar 2023 15:30:49 +0100 Subject: [PATCH 08/41] Fix sorting issues of the editor wide diagnostics and apply diagnostics related review suggestions Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-term/src/application.rs | 21 +++++++++++---------- helix-term/src/commands/lsp.rs | 17 ++++++++++------- helix-term/src/ui/statusline.rs | 2 +- helix-view/src/editor.rs | 2 +- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index e159cb83..728aa46a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -721,7 +721,7 @@ impl Application { )); } } - Notification::PublishDiagnostics(mut params) => { + Notification::PublishDiagnostics(params) => { let path = match params.uri.to_file_path() { Ok(path) => path, Err(_) => { @@ -841,15 +841,10 @@ impl Application { 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 - .diagnostics - .sort_unstable_by_key(|d| (d.severity, d.range.start)); - let diagnostics = params + let mut diagnostics = params .diagnostics .into_iter() - .map(|d| (d, server_id, offset_encoding)) + .map(|d| (d, server_id)) .collect(); // Insert the original lsp::Diagnostics here because we may have no open document @@ -859,10 +854,16 @@ impl Application { 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.extend(diagnostics); + 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); } }; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index efef1211..1a1233a9 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -262,7 +262,7 @@ enum DiagnosticsFormat { fn diag_picker( cx: &Context, - diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize, OffsetEncoding)>>, + diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>, current_path: Option<lsp::Url>, format: DiagnosticsFormat, ) -> FilePicker<PickerDiagnostic> { @@ -272,12 +272,15 @@ fn diag_picker( let mut flat_diag = Vec::new(); for (url, diags) in diagnostics { flat_diag.reserve(diags.len()); - for (diag, _, offset_encoding) in diags { - flat_diag.push(PickerDiagnostic { - url: url.clone(), - diag, - offset_encoding, - }); + + for (diag, ls) in diags { + if let Some(ls) = cx.editor.language_servers.get_by_id(ls) { + flat_diag.push(PickerDiagnostic { + url: url.clone(), + diag, + offset_encoding: ls.offset_encoding(), + }); + } } } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index b10e8076..60997956 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -266,7 +266,7 @@ where .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, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 697d4459..2bd48af8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -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, usize, OffsetEncoding)>>, + pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>, pub diff_providers: DiffProviderRegistry, pub debugger: Option<dap::Client>, From 19f88fc5778b394b90dd9dec89fe3618c252b191 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Fri, 17 Mar 2023 15:41:40 +0100 Subject: [PATCH 09/41] Simplify Display implementation of LanguageServerFeature Co-authored-by: Ivan Tham <pickfire@riseup.net> --- helix-core/src/syntax.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ff4bb6c2..f45a38cc 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -237,25 +237,25 @@ pub enum LanguageServerFeature { impl Display for LanguageServerFeature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LanguageServerFeature::Format => write!(f, "format"), - LanguageServerFeature::GotoDeclaration => write!(f, "goto-declaration"), - LanguageServerFeature::GotoDefinition => write!(f, "goto-definition"), - LanguageServerFeature::GotoTypeDefinition => write!(f, "goto-type-definition"), - LanguageServerFeature::GotoReference => write!(f, "goto-type-definition"), - LanguageServerFeature::GotoImplementation => write!(f, "goto-implementation"), - LanguageServerFeature::SignatureHelp => write!(f, "signature-help"), - LanguageServerFeature::Hover => write!(f, "hover"), - LanguageServerFeature::DocumentHighlight => write!(f, "document-highlight"), - LanguageServerFeature::Completion => write!(f, "completion"), - LanguageServerFeature::CodeAction => write!(f, "code-action"), - LanguageServerFeature::WorkspaceCommand => write!(f, "workspace-command"), - LanguageServerFeature::DocumentSymbols => write!(f, "document-symbols"), - LanguageServerFeature::WorkspaceSymbols => write!(f, "workspace-symbols"), - LanguageServerFeature::Diagnostics => write!(f, "diagnostics"), - LanguageServerFeature::RenameSymbol => write!(f, "rename-symbol"), - LanguageServerFeature::InlayHints => write!(f, "inlay-hints"), - } + write!(f, "{}", match self { + LanguageServerFeature::Format => "format", + LanguageServerFeature::GotoDeclaration => "goto-declaration", + LanguageServerFeature::GotoDefinition => "goto-definition", + LanguageServerFeature::GotoTypeDefinition => "goto-type-definition", + LanguageServerFeature::GotoReference => "goto-type-definition", + LanguageServerFeature::GotoImplementation => "goto-implementation", + LanguageServerFeature::SignatureHelp => "signature-help", + LanguageServerFeature::Hover => "hover", + LanguageServerFeature::DocumentHighlight => "document-highlight", + LanguageServerFeature::Completion => "completion", + LanguageServerFeature::CodeAction => "code-action", + LanguageServerFeature::WorkspaceCommand => "workspace-command", + LanguageServerFeature::DocumentSymbols => "document-symbols", + LanguageServerFeature::WorkspaceSymbols => "workspace-symbols", + LanguageServerFeature::Diagnostics => "diagnostics", + LanguageServerFeature::RenameSymbol => "rename-symbol", + LanguageServerFeature::InlayHints => "inlay-hints", + }) } } From dd2f74794a2ba6c45693d218079349d27828caec Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 16:45:07 +0100 Subject: [PATCH 10/41] Fix error messages when no language server is available Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com> --- helix-term/src/commands/lsp.rs | 4 ++-- helix-view/src/editor.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 1a1233a9..25a54aba 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1130,7 +1130,7 @@ pub fn goto_implementation(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("no language server supports goto-implementation"); + .set_error("No language server supports goto-implementation"); return; } }; @@ -1164,7 +1164,7 @@ pub fn goto_reference(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("language server supports goto-reference"); + .set_error("No language server supports goto-reference"); return; } }; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 2bd48af8..366cc01c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -48,7 +48,7 @@ use helix_core::{ }; use helix_core::{Position, Selection}; use helix_dap as dap; -use helix_lsp::{lsp, OffsetEncoding}; +use helix_lsp::lsp; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; From 0637691eb1fb7e2055fc04a0209be94906c2bd1a Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 17:00:57 +0100 Subject: [PATCH 11/41] Use DoubleEndedIterator instead of collect to Vec for reversing Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-term/src/commands.rs | 2 -- helix-view/src/document.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 27f289f0..14a68490 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3076,8 +3076,6 @@ fn goto_prev_diag(cx: &mut Context) { let diag = doc .shown_diagnostics() - .collect::<Vec<_>>() - .into_iter() .rev() .find(|diag| diag.range.start < cursor_pos) .or_else(|| doc.shown_diagnostics().last()); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 734d76d1..37ddc2b6 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1607,7 +1607,7 @@ impl Document { &self.diagnostics } - pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> { + pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator { let ls_ids: HashSet<_> = self .language_servers_with_feature(LanguageServerFeature::Diagnostics) .iter() From 76b5cab52479daf25ffa0af798c1ebcf6a4f0004 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 20:12:20 +0100 Subject: [PATCH 12/41] Refactored doc.language_servers and doc.language_servers_with_feature to return an iterator and refactor LanguageServerFeature handling to a HashMap (language server name maps to features) Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-core/src/syntax.rs | 97 +++++++++++++++++++++++++------- helix-lsp/src/lib.rs | 20 +++---- helix-term/src/application.rs | 8 +-- helix-term/src/commands.rs | 11 ++-- helix-term/src/commands/lsp.rs | 55 ++++++++---------- helix-term/src/commands/typed.rs | 24 ++++---- helix-term/src/health.rs | 8 +-- helix-term/src/ui/mod.rs | 8 +-- helix-term/src/ui/statusline.rs | 5 +- helix-view/src/document.rs | 52 ++++------------- helix-view/src/editor.rs | 5 +- 11 files changed, 155 insertions(+), 138 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f45a38cc..a4e6d990 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -16,7 +16,7 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap}; use std::{ borrow::Cow, cell::RefCell, - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, fmt::{self, Display}, hash::{Hash, Hasher}, mem::{replace, transmute}, @@ -26,7 +26,7 @@ use std::{ }; 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}; @@ -110,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(default, skip_serializing_if = "Vec::is_empty")] - pub language_servers: Vec<LanguageServerFeatureConfiguration>, + #[serde( + default, + skip_serializing_if = "HashMap::is_empty", + serialize_with = "serialize_lang_features", + deserialize_with = "deserialize_lang_features" + )] + pub language_servers: HashMap<String, LanguageServerFeatures>, #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option<IndentationConfiguration>, @@ -211,7 +216,7 @@ impl<'de> Deserialize<'de> for FileType { } } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] pub enum LanguageServerFeature { Format, @@ -261,18 +266,81 @@ impl Display for LanguageServerFeature { #[derive(Debug, Serialize, Deserialize)] #[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)] -pub enum LanguageServerFeatureConfiguration { +enum LanguageServerFeatureConfiguration { #[serde(rename_all = "kebab-case")] Features { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - only_features: Vec<LanguageServerFeature>, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - except_features: Vec<LanguageServerFeature>, + #[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 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<HashMap<String, 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) => { + (name, LanguageServerFeatures::default()) + } + LanguageServerFeatureConfiguration::Features { + only_features, + except_features, + name, + } => ( + name, + LanguageServerFeatures { + only: only_features, + excluded: except_features, + }, + ), + }) + .collect(); + Ok(res) +} +fn serialize_lang_features<S>( + map: &HashMap<String, LanguageServerFeatures>, + serializer: S, +) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + let mut serializer = serializer.serialize_seq(Some(map.len()))?; + for (name, features) in map { + let features = if features.only.is_empty() && features.excluded.is_empty() { + LanguageServerFeatureConfiguration::Simple(name.to_owned()) + } else { + LanguageServerFeatureConfiguration::Features { + only_features: features.only.clone(), + except_features: features.excluded.clone(), + name: name.to_owned(), + } + }; + serializer.serialize_element(&features)?; + } + serializer.end() +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct LanguageServerConfiguration { @@ -650,15 +718,6 @@ pub struct SoftWrap { pub wrap_at_text_width: Option<bool>, } -impl LanguageServerFeatureConfiguration { - pub fn name(&self) -> &String { - match self { - LanguageServerFeatureConfiguration::Simple(name) => name, - LanguageServerFeatureConfiguration::Features { name, .. } => name, - } - } -} - // Expose loader as Lazy<> global since it's always static? #[derive(Debug)] diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 12e63255..ba0c3fee 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -689,12 +689,10 @@ impl Registry { ) -> Result<Vec<Arc<Client>>> { language_config .language_servers - .iter() - .filter_map(|config| { - let name = config.name().clone(); - + .keys() + .filter_map(|name| { #[allow(clippy::map_entry)] - if self.inner.contains_key(&name) { + if self.inner.contains_key(name) { let client = match self.start_client( name.clone(), language_config, @@ -705,7 +703,10 @@ impl Registry { Ok(client) => client, error => return Some(error), }; - let old_clients = self.inner.insert(name, vec![client.clone()]).unwrap(); + let old_clients = self + .inner + .insert(name.clone(), vec![client.clone()]) + .unwrap(); // TODO what if there are different language servers for different workspaces, // I think the language servers will be stopped without being restarted, which is not intended @@ -742,9 +743,8 @@ impl Registry { ) -> Result<Vec<Arc<Client>>> { language_config .language_servers - .iter() - .map(|features| { - let name = features.name(); + .keys() + .map(|name| { if let Some(clients) = self.inner.get_mut(name) { if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, client)| { client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0) @@ -759,7 +759,7 @@ impl Registry { root_dirs, enable_snippets, )?; - let clients = self.inner.entry(features.name().clone()).or_default(); + let clients = self.inner.entry(name.clone()).or_default(); clients.push(client.clone()); Ok(client) }) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 728aa46a..83473179 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -699,9 +699,10 @@ impl Application { tokio::spawn(language_server.did_change_configuration(config.clone())); } - let docs = self.editor.documents().filter(|doc| { - doc.language_servers().iter().any(|l| l.id() == server_id) - }); + let docs = self + .editor + .documents() + .filter(|doc| doc.language_servers().any(|l| l.id() == server_id)); // trigger textDocument/didOpen for docs that are already open for doc in docs { @@ -970,7 +971,6 @@ impl Application { .filter_map(|doc| { if doc .language_servers() - .iter() .any(|server| server.id() == server_id) { doc.clear_diagnostics(server_id); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 14a68490..060c9d83 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3235,7 +3235,6 @@ pub mod insert { let doc = doc_mut!(cx.editor); let trigger_completion = doc .language_servers_with_feature(LanguageServerFeature::Completion) - .iter() .any(|ls| { let capabilities = ls.capabilities(); @@ -3264,7 +3263,6 @@ pub mod insert { // TODO support multiple language servers (not just the first that is found) let future = doc .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .iter() .find_map(|ls| { let capabilities = ls.capabilities(); @@ -4067,10 +4065,8 @@ fn format_selections(cx: &mut Context) { .set_error("format_selections only supports a single selection for now"); return; } - - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::Format) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let ranges: Vec<lsp::Range> = doc @@ -4091,7 +4087,9 @@ fn format_selections(cx: &mut Context) { None, )?; Some((future, offset_encoding)) - }) { + }); + + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -4247,7 +4245,6 @@ pub fn completion(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::Completion) - .iter() // TODO this should probably already been filtered in something like "language_servers_with_feature" .filter_map(|language_server| { let language_server_id = language_server.id(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 25a54aba..6553ce16 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -353,7 +353,6 @@ pub fn symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) - .iter() .filter_map(|ls| { let request = ls.document_symbols(doc.identifier())?; Some((request, ls.offset_encoding(), doc.identifier())) @@ -420,7 +419,6 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let doc = doc!(editor); let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) - .iter() .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) .map(|(request, offset_encoding)| async move { let json = request.await?; @@ -581,7 +579,6 @@ pub fn code_action(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::CodeAction) - .iter() // 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(); @@ -1034,15 +1031,15 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo // TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity... pub fn goto_declaration(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoDeclaration) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_declaration(doc.identifier(), pos, None)?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1062,15 +1059,15 @@ pub fn goto_declaration(cx: &mut Context) { pub fn goto_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoDefinition) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_definition(doc.identifier(), pos, None)?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1090,15 +1087,15 @@ pub fn goto_definition(cx: &mut Context) { pub fn goto_type_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_type_definition(doc.identifier(), pos, None)?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1118,15 +1115,15 @@ pub fn goto_type_definition(cx: &mut Context) { pub fn goto_implementation(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoImplementation) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.goto_implementation(doc.identifier(), pos, None)?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1147,9 +1144,8 @@ pub fn goto_implementation(cx: &mut Context) { pub fn goto_reference(cx: &mut Context) { let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::GotoReference) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); @@ -1160,7 +1156,8 @@ pub fn goto_reference(cx: &mut Context) { None, )?; Some((future, offset_encoding)) - }) { + }); + let (future, offset_encoding) = match future_offset_encoding { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor @@ -1192,13 +1189,14 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { let (view, doc) = current!(cx.editor); // TODO merge multiple language server signature help into one instead of just taking the first language server that supports it - let future = match doc + let future = doc .language_servers_with_feature(LanguageServerFeature::SignatureHelp) - .iter() .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 future = match future { Some(future) => future.boxed(), None => { // Do not show the message if signature help was invoked @@ -1328,7 +1326,6 @@ pub fn hover(cx: &mut Context) { // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier let request = doc .language_servers_with_feature(LanguageServerFeature::Hover) - .iter() .find_map(|language_server| { let pos = doc.position(view.id, language_server.offset_encoding()); language_server.text_document_hover(doc.identifier(), pos, None) @@ -1436,7 +1433,6 @@ pub fn rename_symbol(cx: &mut Context) { let (view, doc) = current!(cx.editor); let request = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .iter() .find_map(|language_server| { if let Some(language_server_id) = language_server_id { if language_server.id() != language_server_id { @@ -1475,7 +1471,6 @@ pub fn rename_symbol(cx: &mut Context) { let prepare_rename_request = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); @@ -1516,17 +1511,17 @@ pub fn rename_symbol(cx: &mut Context) { pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let (future, offset_encoding) = match doc + let future_offset_encoding = doc .language_servers_with_feature(LanguageServerFeature::DocumentHighlight) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let pos = doc.position(view.id, offset_encoding); let future = language_server.text_document_document_highlight(doc.identifier(), pos, None)?; Some((future, offset_encoding)) - }) { - Some(future) => future, + }); + let (future, offset_encoding) = match future_offset_encoding { + Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor .set_error("No language server supports document-highlight"); @@ -1587,8 +1582,8 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); - let language_server = language_servers.iter().find(|language_server| { + let mut language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); + let language_server = language_servers.find(|language_server| { matches!( language_server.capabilities().inlay_hint_provider, Some( diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index b78de772..38058ed5 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1330,14 +1330,16 @@ fn lsp_workspace_command( return Ok(()); } let doc = doc!(cx.editor); - let language_servers = - doc.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); - let (language_server_id, options) = match language_servers.iter().find_map(|ls| { - ls.capabilities() - .execute_command_provider - .as_ref() - .map(|options| (ls.id(), options)) - }) { + let id_options = doc + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) + .find_map(|ls| { + ls.capabilities() + .execute_command_provider + .as_ref() + .map(|options| (ls.id(), options)) + }); + + let (language_server_id, options) = match id_options { Some(id_options) => id_options, None => { cx.editor.set_status( @@ -1346,6 +1348,7 @@ fn lsp_workspace_command( return Ok(()); } }; + if args.is_empty() { let commands = options .commands @@ -1445,7 +1448,6 @@ fn lsp_stop( // I'm not sure if this is really what we want let ls_shutdown_names = doc .language_servers() - .iter() .map(|ls| ls.name()) .collect::<Vec<_>>(); @@ -1459,7 +1461,6 @@ fn lsp_stop( .filter_map(|doc| { let doc_active_ls_ids: Vec<_> = doc .language_servers() - .iter() .filter(|ls| !ls_shutdown_names.contains(&ls.name())) .map(|ls| ls.id()) .collect(); @@ -1472,7 +1473,7 @@ fn lsp_stop( .map(Clone::clone) .collect(); - if active_clients.len() != doc.language_servers().len() { + if active_clients.len() != doc.language_servers().count() { Some((doc.id(), active_clients)) } else { None @@ -1485,7 +1486,6 @@ fn lsp_stop( let stopped_clients: Vec<_> = doc .language_servers() - .iter() .filter(|ls| { !active_clients .iter() diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6b9f8517..5b22ea55 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -194,10 +194,10 @@ pub fn languages_all() -> std::io::Result<()> { // TODO multiple language servers (check binary for each supported language server, not just the first) - let lsp = lang.language_servers.first().and_then(|lsp| { + let lsp = lang.language_servers.keys().next().and_then(|ls_name| { syn_loader_conf .language_server - .get(lsp.name()) + .get(ls_name) .map(|config| config.command.clone()) }); check_binary(lsp); @@ -271,10 +271,10 @@ pub fn language(lang_str: String) -> std::io::Result<()> { // TODO multiple language servers probe_protocol( "language server", - lang.language_servers.first().and_then(|lsp| { + lang.language_servers.keys().next().and_then(|ls_name| { syn_loader_conf .language_server - .get(lsp.name()) + .get(ls_name) .map(|config| config.command.clone()) }), )?; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 118836c0..6f7ed174 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -394,13 +394,11 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { let matcher = Matcher::default(); - let language_servers = - doc!(editor).language_servers_with_feature(LanguageServerFeature::WorkspaceCommand); - let options = match language_servers - .into_iter() + let options = match doc!(editor) + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) { - Some(id_options) => id_options, + Some(options) => options, None => { return vec![]; } diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 60997956..4aa64634 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -202,11 +202,10 @@ fn render_lsp_spinner<F>(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { - let language_servers = context.doc.language_servers(); + let language_server = context.doc.language_servers().next(); write( context, - language_servers - .first() + language_server .and_then(|srv| { context .spinners diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 37ddc2b6..4b075293 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -6,7 +6,7 @@ use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; -use helix_core::syntax::{Highlight, LanguageServerFeature, LanguageServerFeatureConfiguration}; +use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::text_annotations::{InlineAnnotation, TextAnnotations}; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -734,7 +734,6 @@ impl Document { // finds first language server that supports formatting and then formats let (offset_encoding, request) = self .language_servers_with_feature(LanguageServerFeature::Format) - .iter() .find_map(|language_server| { let offset_encoding = language_server.offset_encoding(); let request = language_server.text_document_formatting( @@ -1437,54 +1436,24 @@ impl Document { self.version } - /// Language servers that have been initialized. - pub fn language_servers(&self) -> Vec<&helix_lsp::Client> { + pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> { self.language_servers .iter() .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None }) - .collect() } // TODO filter also based on LSP capabilities? pub fn language_servers_with_feature( &self, feature: LanguageServerFeature, - ) -> Vec<&helix_lsp::Client> { - let language_servers = self.language_servers(); - - let language_config = match self.language_config() { - Some(language_config) => language_config, - None => return Vec::new(), - }; - - // O(n^2) but since language_servers will be of very small length, - // I don't see the necessity to optimize - language_config - .language_servers - .iter() - .filter_map(|c| match c { - LanguageServerFeatureConfiguration::Simple(name) => language_servers - .iter() - .find(|ls| ls.name() == name) - .copied(), - LanguageServerFeatureConfiguration::Features { - only_features, - except_features, - name, - } => { - if (only_features.is_empty() || only_features.contains(&feature)) - && !except_features.contains(&feature) - { - language_servers - .iter() - .find(|ls| ls.name() == name) - .copied() - } else { - None - } - } - }) - .collect() + ) -> impl Iterator<Item = &helix_lsp::Client> { + self.language_servers().filter(move |server| { + self.language_config() + .and_then(|config| config.language_servers.get(server.name())) + .map_or(false, |server_features| { + server_features.has_feature(feature) + }) + }) } pub fn diff_handle(&self) -> Option<&DiffHandle> { @@ -1610,7 +1579,6 @@ impl Document { pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator { let ls_ids: HashSet<_> = self .language_servers_with_feature(LanguageServerFeature::Diagnostics) - .iter() .map(|ls| ls.id()) .collect(); self.diagnostics diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 366cc01c..bca97815 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE @@ -1129,7 +1129,8 @@ impl Editor { if let Some(language_servers) = language_servers { // only spawn new lang servers if the servers aren't the same - let doc_language_servers = doc.language_servers(); + // TODO simplify? + let doc_language_servers = doc.language_servers().collect::<Vec<_>>(); let spawn_new_servers = language_servers.len() != doc_language_servers.len() || language_servers .iter() From ec2f9091a06d07a8c4faa7533b814bdf59e36c35 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 20:32:34 +0100 Subject: [PATCH 13/41] Simplify Display implementation for LanguageServerFeature --- helix-core/src/syntax.rs | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a4e6d990..ce26a1bc 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -242,25 +242,27 @@ pub enum LanguageServerFeature { impl Display for LanguageServerFeature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", match self { - LanguageServerFeature::Format => "format", - LanguageServerFeature::GotoDeclaration => "goto-declaration", - LanguageServerFeature::GotoDefinition => "goto-definition", - LanguageServerFeature::GotoTypeDefinition => "goto-type-definition", - LanguageServerFeature::GotoReference => "goto-type-definition", - LanguageServerFeature::GotoImplementation => "goto-implementation", - LanguageServerFeature::SignatureHelp => "signature-help", - LanguageServerFeature::Hover => "hover", - LanguageServerFeature::DocumentHighlight => "document-highlight", - LanguageServerFeature::Completion => "completion", - LanguageServerFeature::CodeAction => "code-action", - LanguageServerFeature::WorkspaceCommand => "workspace-command", - LanguageServerFeature::DocumentSymbols => "document-symbols", - LanguageServerFeature::WorkspaceSymbols => "workspace-symbols", - LanguageServerFeature::Diagnostics => "diagnostics", - LanguageServerFeature::RenameSymbol => "rename-symbol", - LanguageServerFeature::InlayHints => "inlay-hints", - }) + 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}",) } } From 1122928c2adef0a1c34e90d140c61f84beeb7418 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 20:33:17 +0100 Subject: [PATCH 14/41] Add method doc.supports_language_server for better readability --- helix-term/src/application.rs | 7 ++----- helix-view/src/document.rs | 4 ++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 83473179..dbb873e0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -702,7 +702,7 @@ impl Application { let docs = self .editor .documents() - .filter(|doc| doc.language_servers().any(|l| l.id() == server_id)); + .filter(|doc| doc.supports_language_server(server_id)); // trigger textDocument/didOpen for docs that are already open for doc in docs { @@ -969,10 +969,7 @@ impl Application { .editor .documents_mut() .filter_map(|doc| { - if doc - .language_servers() - .any(|server| server.id() == server_id) - { + if doc.supports_language_server(server_id) { doc.clear_diagnostics(server_id); doc.url() } else { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 4b075293..5fec3361 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1456,6 +1456,10 @@ impl Document { }) } + pub fn supports_language_server(&self, id: usize) -> bool { + self.language_servers().any(|l| l.id() == id) + } + pub fn diff_handle(&self) -> Option<&DiffHandle> { self.diff_handle.as_ref() } From 9639f42766a18983dc2298e60bf47a748845bde7 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 21:10:10 +0100 Subject: [PATCH 15/41] Refactor doc.shown_diagnostics to avoid an extra HashSet Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-view/src/document.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 5fec3361..0f800209 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -16,7 +16,7 @@ use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; use std::cell::Cell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; @@ -1581,13 +1581,16 @@ impl Document { } pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator { - let ls_ids: HashSet<_> = self - .language_servers_with_feature(LanguageServerFeature::Diagnostics) - .map(|ls| ls.id()) - .collect(); - self.diagnostics - .iter() - .filter(move |d| ls_ids.contains(&d.language_server_id)) + self.diagnostics.iter().filter(|d| { + self.language_servers() + .find(|ls| ls.id() == d.language_server_id) + .and_then(|ls| { + let config = self.language_config()?; + let features = config.language_servers.get(ls.name())?; + Some(features.has_feature(LanguageServerFeature::Diagnostics)) + }) + == Some(true) + }) } pub fn replace_diagnostics( From d9630506218f713cbacfa198443c85e9881b8a8b Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 22:53:09 +0100 Subject: [PATCH 16/41] Format/fix language docs a bit --- book/src/languages.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/book/src/languages.md b/book/src/languages.md index 3328c610..3f244566 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -140,10 +140,14 @@ config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenth ### 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: @@ -162,12 +166,12 @@ name = "typescript" language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ] ``` -Each requested LSP feature is priorized in the order of the `language-servers` array. +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`). If no `except-features` or `only-features` is given all features for the language server are enabled. If a language server itself doesn't support a feature the next language server array entry will be tried (and so on). -The list of supported features are: +The list of supported features is: - `format` - `goto-definition` From 60a6af1fea64381e66ec935d61ff9250491823a5 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 23:13:58 +0100 Subject: [PATCH 17/41] Remove boilerplate in the goto methods by generically composing functions --- helix-term/src/commands/lsp.rs | 189 +++++++++++---------------------- helix-view/src/document.rs | 19 +++- 2 files changed, 82 insertions(+), 126 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 6553ce16..9eaead95 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -6,7 +6,7 @@ use helix_lsp::{ 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; @@ -1028,151 +1028,90 @@ fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Lo } } -// TODO find a way to reduce boilerplate of all the goto functions, without unnecessary complexity... -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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::GotoDeclaration) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.goto_declaration(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No language server supports goto-declaration"); - return; - } - }; + if let Some((future, offset_encoding)) = + doc.run_on_first_supported_language_server(view.id, feature, |ls, encoding, pos, doc_id| { + Some((request_provider(ls, pos, doc_id)?, encoding)) + }) + { + cx.callback( + future, + move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { + let items = to_locations(response); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); + } else { + cx.editor.set_error("No language server supports {feature}"); + } +} - cx.callback( - future, - move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, +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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::GotoDefinition) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.goto_definition(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No language server supports 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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::GotoTypeDefinition) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.goto_type_definition(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No language server supports 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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::GotoImplementation) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.goto_implementation(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No language server supports 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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::GotoReference) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.goto_reference( - doc.identifier(), - pos, - config.lsp.goto_reference_include_declaration, - None, - )?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No language server supports goto-reference"); - return; - } - }; - - cx.callback( - future, - move |editor, compositor, response: Option<Vec<lsp::Location>>| { - let items = response.unwrap_or_default(); - goto_impl(editor, compositor, items, offset_encoding); + if let Some((future, offset_encoding)) = doc.run_on_first_supported_language_server( + view.id, + LanguageServerFeature::GotoReference, + |ls, encoding, pos, doc_id| { + Some(( + ls.goto_reference( + doc_id, + pos, + config.lsp.goto_reference_include_declaration, + None, + )?, + encoding, + )) }, - ); + ) { + cx.callback( + future, + move |editor, compositor, response: Option<Vec<lsp::Location>>| { + let items = response.unwrap_or_default(); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); + } else { + cx.editor + .set_error("No language server supports goto-reference"); + } } #[derive(PartialEq, Eq, Clone, Copy)] diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 0f800209..3fd271eb 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -580,7 +580,7 @@ where *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::lsp; +use helix_lsp::{lsp, Client, OffsetEncoding}; use url::Url; impl Document { @@ -1460,6 +1460,23 @@ impl Document { self.language_servers().any(|l| l.id() == id) } + pub fn run_on_first_supported_language_server<T, P>( + &self, + view_id: ViewId, + feature: LanguageServerFeature, + request_provider: P, + ) -> Option<T> + where + P: Fn(&Client, OffsetEncoding, lsp::Position, lsp::TextDocumentIdentifier) -> Option<T>, + { + self.language_servers_with_feature(feature) + .find_map(|language_server| { + let offset_encoding = language_server.offset_encoding(); + let pos = self.position(view_id, offset_encoding); + request_provider(language_server, offset_encoding, pos, self.identifier()) + }) + } + pub fn diff_handle(&self) -> Option<&DiffHandle> { self.diff_handle.as_ref() } From 7d20740b5b2e115e3075251204ee1ff1901166df Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sat, 18 Mar 2023 23:41:38 +0100 Subject: [PATCH 18/41] Fix docgen and lsp-stop documentation --- book/src/generated/typable-cmd.md | 2 +- helix-term/src/commands/typed.rs | 2 +- xtask/src/docgen.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index cc2e87ea..0f488dc0 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -51,7 +51,7 @@ | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | | `:lsp-restart` | Restarts the language servers used by the current doc | -| `:lsp-stop` | Stops the Language Server that is in use by the current doc | +| `:lsp-stop` | Stops the language servers that are used by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `: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. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 38058ed5..5f94b7c4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2635,7 +2635,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ 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(), }, diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index 3bd0592e..d360b4bd 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -97,8 +97,8 @@ pub fn lang_features() -> Result<String, DynError> { } row.push( lc.language_servers - .iter() - .filter_map(|ls| config.language_server.get(ls.name())) + .keys() + .filter_map(|ls| config.language_server.get(ls)) .map(|s| md_mono(&s.command.clone())) .collect::<Vec<_>>() .join(", "), From 58c913ce4567a95c52808d78245eb1dac806971c Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sun, 19 Mar 2023 19:26:39 +0100 Subject: [PATCH 19/41] Simplify 'lsp_stop' command --- helix-term/src/commands/typed.rs | 53 +++----------------------------- helix-view/src/document.rs | 7 +++++ 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 5f94b7c4..8cfc9fd2 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1442,62 +1442,19 @@ fn lsp_stop( return Ok(()); } - let doc = doc!(cx.editor); - - // TODO this stops language servers which may be used in another doc/language type that uses the same language servers - // I'm not sure if this is really what we want - let ls_shutdown_names = doc + let ls_shutdown_names = doc!(cx.editor) .language_servers() - .map(|ls| ls.name()) + .map(|ls| ls.name().to_string()) .collect::<Vec<_>>(); for ls_name in &ls_shutdown_names { cx.editor.language_servers.stop(ls_name); - } - let doc_ids_active_clients: Vec<_> = cx - .editor - .documents() - .filter_map(|doc| { - let doc_active_ls_ids: Vec<_> = doc - .language_servers() - .filter(|ls| !ls_shutdown_names.contains(&ls.name())) - .map(|ls| ls.id()) - .collect(); - - let active_clients: Vec<_> = cx - .editor - .language_servers - .iter_clients() - .filter(|client| doc_active_ls_ids.contains(&client.id())) - .map(Clone::clone) - .collect(); - - if active_clients.len() != doc.language_servers().count() { - Some((doc.id(), active_clients)) - } else { - None + for doc in cx.editor.documents_mut() { + if let Some(client) = doc.remove_language_server_by_name(ls_name) { + doc.clear_diagnostics(client.id()); } - }) - .collect(); - - for (doc_id, active_clients) in doc_ids_active_clients { - let doc = cx.editor.documents.get_mut(&doc_id).unwrap(); - - let stopped_clients: Vec<_> = doc - .language_servers() - .filter(|ls| { - !active_clients - .iter() - .any(|active_ls| active_ls.id() == ls.id()) - }) - .map(|ls| ls.id()) - .collect(); // is necessary because of borrow-checking - - for client_id in stopped_clients { - doc.clear_diagnostics(client_id) } - doc.set_language_servers(active_clients); } Ok(()) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3fd271eb..49eb13a0 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1442,6 +1442,13 @@ impl Document { .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None }) } + pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> { + match self.language_servers.iter().position(|l| l.name() == name) { + Some(index) => Some(self.language_servers.remove(index)), + None => None, + } + } + // TODO filter also based on LSP capabilities? pub fn language_servers_with_feature( &self, From b1199c552be39eec8cb428310c8bb2a952454b04 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sun, 19 Mar 2023 21:22:29 +0100 Subject: [PATCH 20/41] Remove symbol picker is_empty check --- helix-term/src/commands/lsp.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 9eaead95..9e9639bf 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -399,11 +399,7 @@ pub fn symbol_picker(cx: &mut Context) { while let Some(mut lsp_items) = futures.try_next().await? { symbols.append(&mut lsp_items); } - let call = move |editor: &mut Editor, compositor: &mut Compositor| { - if symbols.is_empty() { - editor.set_error("No symbols available"); - return; - } + let call = move |_editor: &mut Editor, compositor: &mut Compositor| { let picker = sym_picker(symbols, current_url); compositor.push(Box::new(overlaid(picker))) }; From 2eeac10755e6b88e3d5861dee7e2016a10c01c9f Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sun, 19 Mar 2023 23:37:41 +0100 Subject: [PATCH 21/41] Refactor doc language servers to a HashMap, and the config to use a Vec to retain order --- helix-core/src/syntax.rs | 34 +++++++++++----------- helix-lsp/src/lib.rs | 23 ++++++++------- helix-term/src/health.rs | 8 +++--- helix-view/src/document.rs | 48 ++++++++++++++++--------------- helix-view/src/editor.rs | 58 +++++++++++++++++++------------------- 5 files changed, 88 insertions(+), 83 deletions(-) diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ce26a1bc..3fa7994d 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -112,11 +112,11 @@ pub struct LanguageConfiguration { // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 #[serde( default, - skip_serializing_if = "HashMap::is_empty", + skip_serializing_if = "Vec::is_empty", serialize_with = "serialize_lang_features", deserialize_with = "deserialize_lang_features" )] - pub language_servers: HashMap<String, LanguageServerFeatures>, + pub language_servers: Vec<LanguageServerFeatures>, #[serde(skip_serializing_if = "Option::is_none")] pub indent: Option<IndentationConfiguration>, @@ -282,19 +282,20 @@ enum LanguageServerFeatureConfiguration { #[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) + (self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature) } } fn deserialize_lang_features<'de, D>( deserializer: D, -) -> Result<HashMap<String, LanguageServerFeatures>, D::Error> +) -> Result<Vec<LanguageServerFeatures>, D::Error> where D: serde::Deserializer<'de>, { @@ -302,40 +303,39 @@ where let res = raw .into_iter() .map(|config| match config { - LanguageServerFeatureConfiguration::Simple(name) => { - (name, LanguageServerFeatures::default()) - } + LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures { + name, + ..Default::default() + }, LanguageServerFeatureConfiguration::Features { only_features, except_features, name, - } => ( + } => LanguageServerFeatures { name, - LanguageServerFeatures { - only: only_features, - excluded: except_features, - }, - ), + only: only_features, + excluded: except_features, + }, }) .collect(); Ok(res) } fn serialize_lang_features<S>( - map: &HashMap<String, LanguageServerFeatures>, + map: &Vec<LanguageServerFeatures>, serializer: S, ) -> Result<S::Ok, S::Error> where S: serde::Serializer, { let mut serializer = serializer.serialize_seq(Some(map.len()))?; - for (name, features) in map { + for features in map { let features = if features.only.is_empty() && features.excluded.is_empty() { - LanguageServerFeatureConfiguration::Simple(name.to_owned()) + LanguageServerFeatureConfiguration::Simple(features.name.to_owned()) } else { LanguageServerFeatureConfiguration::Features { only_features: features.only.clone(), except_features: features.excluded.clone(), - name: name.to_owned(), + name: features.name.to_owned(), } }; serializer.serialize_element(&features)?; diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index ba0c3fee..6b4bb430 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -12,7 +12,7 @@ pub use lsp_types as lsp; use futures_util::stream::select_all::SelectAll; use helix_core::{ path, - syntax::{LanguageConfiguration, LanguageServerConfiguration}, + syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures}, }; use tokio::sync::mpsc::UnboundedReceiver; @@ -26,7 +26,7 @@ use thiserror::Error; use tokio_stream::wrappers::UnboundedReceiverStream; pub type Result<T> = core::result::Result<T, Error>; -type LanguageServerName = String; +pub type LanguageServerName = String; #[derive(Error, Debug)] pub enum Error { @@ -689,9 +689,9 @@ impl Registry { ) -> Result<Vec<Arc<Client>>> { language_config .language_servers - .keys() - .filter_map(|name| { - #[allow(clippy::map_entry)] + .iter() + .filter_map(|LanguageServerFeatures { name, .. }| { + // #[allow(clippy::map_entry)] if self.inner.contains_key(name) { let client = match self.start_client( name.clone(), @@ -740,17 +740,20 @@ impl Registry { doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result<Vec<Arc<Client>>> { + ) -> Result<HashMap<LanguageServerName, Arc<Client>>> { language_config .language_servers - .keys() - .map(|name| { + .iter() + .map(|LanguageServerFeatures { name, .. }| { if let Some(clients) = self.inner.get_mut(name) { + // clients.find( + 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(client.clone()); + return Ok((name.to_owned(), client.clone())); } + // return Ok((name.clone(), clients.clone())); } let client = self.start_client( name.clone(), @@ -761,7 +764,7 @@ impl Registry { )?; let clients = self.inner.entry(name.clone()).or_default(); clients.push(client.clone()); - Ok(client) + Ok((name.clone(), client)) }) .collect() } diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 5b22ea55..8f921877 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -194,10 +194,10 @@ pub fn languages_all() -> std::io::Result<()> { // TODO multiple language servers (check binary for each supported language server, not just the first) - let lsp = lang.language_servers.keys().next().and_then(|ls_name| { + let lsp = lang.language_servers.first().and_then(|ls| { syn_loader_conf .language_server - .get(ls_name) + .get(&ls.name) .map(|config| config.command.clone()) }); check_binary(lsp); @@ -271,10 +271,10 @@ pub fn language(lang_str: String) -> std::io::Result<()> { // TODO multiple language servers probe_protocol( "language server", - lang.language_servers.keys().next().and_then(|ls_name| { + lang.language_servers.first().and_then(|ls| { syn_loader_conf .language_server - .get(ls_name) + .get(&ls.name) .map(|config| config.command.clone()) }), )?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 49eb13a0..27f5d279 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -180,7 +180,7 @@ pub struct Document { pub(crate) modified_since_accessed: bool, diagnostics: Vec<Diagnostic>, - language_servers: Vec<Arc<helix_lsp::Client>>, + pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>, diff_handle: Option<DiffHandle>, version_control_head: Option<Arc<ArcSwap<Box<str>>>>, @@ -580,7 +580,7 @@ where *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::{lsp, Client, OffsetEncoding}; +use helix_lsp::{lsp, Client, LanguageServerName, OffsetEncoding}; use url::Url; impl Document { @@ -616,7 +616,7 @@ impl Document { last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, - language_servers: Vec::new(), + language_servers: HashMap::new(), diff_handle: None, config, version_control_head: None, @@ -850,7 +850,7 @@ impl Document { text: text.clone(), }; - for language_server in language_servers { + for (_, language_server) in language_servers { if !language_server.is_initialized() { return Ok(event); } @@ -1006,11 +1006,6 @@ impl Document { Ok(()) } - /// Set the LSP. - pub fn set_language_servers(&mut self, language_servers: Vec<Arc<helix_lsp::Client>>) { - self.language_servers = language_servers; - } - /// Select text within the [`Document`]. pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { // TODO: use a transaction? @@ -1437,16 +1432,17 @@ impl Document { } pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> { - self.language_servers - .iter() - .filter_map(|l| if l.is_initialized() { Some(&**l) } else { None }) + self.language_servers.values().filter_map(|l| { + if l.is_initialized() { + Some(&**l) + } else { + None + } + }) } pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> { - match self.language_servers.iter().position(|l| l.name() == name) { - Some(index) => Some(self.language_servers.remove(index)), - None => None, - } + self.language_servers.remove(name) } // TODO filter also based on LSP capabilities? @@ -1454,12 +1450,15 @@ impl Document { &self, feature: LanguageServerFeature, ) -> impl Iterator<Item = &helix_lsp::Client> { - self.language_servers().filter(move |server| { - self.language_config() - .and_then(|config| config.language_servers.get(server.name())) - .map_or(false, |server_features| { - server_features.has_feature(feature) - }) + 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() && features.has_feature(feature) { + Some(ls) + } else { + None + } + }) }) } @@ -1610,7 +1609,10 @@ impl Document { .find(|ls| ls.id() == d.language_server_id) .and_then(|ls| { let config = self.language_config()?; - let features = config.language_servers.get(ls.name())?; + let features = config + .language_servers + .iter() + .find(|features| features.name == ls.name())?; Some(features.has_feature(LanguageServerFeature::Diagnostics)) }) == Some(true) diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index bca97815..ca2144fd 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -689,7 +689,7 @@ pub struct WhitespaceCharacters { impl Default for WhitespaceCharacters { fn default() -> Self { Self { - space: '·', // U+00B7 + space: '·', // U+00B7 nbsp: '⍽', // U+237D tab: '→', // U+2192 newline: '⏎', // U+23CE @@ -1103,9 +1103,9 @@ impl Editor { 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; @@ -1124,37 +1124,37 @@ impl Editor { .ok() }); - let doc = self.document_mut(doc_id)?; - let doc_url = doc.url()?; - if let Some(language_servers) = language_servers { - // only spawn new lang servers if the servers aren't the same - // TODO simplify? - let doc_language_servers = doc.language_servers().collect::<Vec<_>>(); - let spawn_new_servers = language_servers.len() != doc_language_servers.len() - || language_servers - .iter() - .zip(doc_language_servers.iter()) - .any(|(l, dl)| l.id() != dl.id()); - if spawn_new_servers { - for doc_language_server in doc_language_servers { - tokio::spawn(doc_language_server.text_document_did_close(doc.identifier())); - } + let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); - let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); + // only spawn new language servers if the servers aren't the same - for language_server in &language_servers { - // TODO: this now races with on_init code if the init happens too quickly - tokio::spawn(language_server.text_document_did_open( - doc_url.clone(), - doc.version(), - doc.text(), - language_id.clone(), - )); - } + let doc_language_servers_not_in_registry = + doc.language_servers.iter().filter(|(name, doc_ls)| { + !language_servers.contains_key(*name) + || language_servers[*name].id() != doc_ls.id() + }); - doc.set_language_servers(language_servers); + 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.contains_key(*name) + || doc.language_servers[*name].id() != 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.clone(), + doc.version(), + doc.text(), + language_id.clone(), + )); + } + + doc.language_servers = language_servers; } Some(()) } From 1d5d5dab4718b9db40a20037c1cb1edf66f7991a Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 00:02:41 +0100 Subject: [PATCH 22/41] Remove offset_encoding in CompletionItem --- helix-term/src/commands.rs | 1 - helix-term/src/ui/completion.rs | 31 ++++++++++++++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 060c9d83..772db6f8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4269,7 +4269,6 @@ pub fn completion(cx: &mut Context) { .map(|item| CompletionItem { item, language_server_id, - offset_encoding, resolved: false, }) .collect(); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 980bc8b6..e62efdac 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -88,7 +88,6 @@ impl menu::Item for CompletionItem { pub struct CompletionItem { pub item: lsp::CompletionItem, pub language_server_id: usize, - pub offset_encoding: OffsetEncoding, pub resolved: bool, } @@ -121,6 +120,7 @@ impl Completion { doc: &Document, view_id: ViewId, item: &CompletionItem, + offset_encoding: OffsetEncoding, trigger_offset: usize, include_placeholder: bool, replace_mode: bool, @@ -143,8 +143,6 @@ impl Completion { } }; - let offset_encoding = item.offset_encoding; - let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{ return Transaction::new(doc.text()); }; @@ -238,8 +236,21 @@ impl Completion { // always present here let item = item.unwrap(); - let transaction = - item_to_transaction(doc, view.id, item, trigger_offset, true, replace_mode); + let offset_encoding = editor + .language_servers + .get_by_id(item.language_server_id) + .expect("language server disappeared between completion request and application") + .offset_encoding(); + + let transaction = item_to_transaction( + doc, + view.id, + item, + offset_encoding, + trigger_offset, + true, + replace_mode, + ); doc.apply_temporary(&transaction, view.id); } PromptEvent::Validate => { @@ -251,6 +262,12 @@ impl Completion { // always present here let mut item = item.unwrap().clone(); + let offset_encoding = editor + .language_servers + .get_by_id(item.language_server_id) + .expect("language server disappeared between completion request and application") + .offset_encoding(); + let language_server = editor .language_servers .get_by_id(item.language_server_id) @@ -270,6 +287,7 @@ impl Completion { doc, view.id, &item, + offset_encoding, trigger_offset, false, replace_mode, @@ -287,7 +305,7 @@ impl Completion { let transaction = util::generate_transaction_from_edits( doc.text(), additional_edits, - item.offset_encoding, // TODO: should probably transcode in Client + offset_encoding, // TODO: should probably transcode in Client ); doc.apply(&transaction, view.id); } @@ -414,7 +432,6 @@ impl Completion { let resolved_item = CompletionItem { item: resolved_item, language_server_id: current_item.language_server_id, - offset_encoding: current_item.offset_encoding, resolved: true, }; From 8ab6d7be5e1b6f36215820ef616c2a9feb2306fa Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 00:08:24 +0100 Subject: [PATCH 23/41] Use let else instead of variable and fix some error messages Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-term/src/commands.rs | 2 +- helix-term/src/commands/lsp.rs | 20 +++++++++++--------- helix-term/src/commands/typed.rs | 18 +++++++----------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 772db6f8..e2a5975b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4093,7 +4093,7 @@ fn format_selections(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("No language server supports range formatting"); + .set_error("No configured language server supports range formatting"); return; } }; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 9e9639bf..a1bd291c 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -389,7 +389,7 @@ pub fn symbol_picker(cx: &mut Context) { if futures.is_empty() { cx.editor - .set_error("No Language server does support document symbols"); + .set_error("No configured language server supports document symbols"); return; } @@ -433,7 +433,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { .collect(); if futures.is_empty() { - editor.set_error("No Language server does support workspace symbols"); + editor.set_error("No configured language server supports workspace symbols"); } async move { @@ -663,7 +663,7 @@ pub fn code_action(cx: &mut Context) { if futures.is_empty() { cx.editor - .set_error("No Language server does support code actions"); + .set_error("No configured language server supports code actions"); return; } @@ -1043,7 +1043,8 @@ where }, ); } else { - cx.editor.set_error("No language server supports {feature}"); + cx.editor + .set_error("No configured language server supports {feature}"); } } @@ -1106,7 +1107,7 @@ pub fn goto_reference(cx: &mut Context) { ); } else { cx.editor - .set_error("No language server supports goto-reference"); + .set_error("No configured language server supports goto-reference"); } } @@ -1138,7 +1139,7 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { // automatically on backspace, trigger characters, etc. if invoked == SignatureHelpInvoked::Manual { cx.editor - .set_error("No language server supports signature-help"); + .set_error("No configured language server supports signature-help"); } return; } @@ -1269,7 +1270,8 @@ pub fn hover(cx: &mut Context) { let future = match request { Some(future) => future, None => { - cx.editor.set_error("No language server supports hover"); + cx.editor + .set_error("No configured language server supports hover"); return; } }; @@ -1393,7 +1395,7 @@ pub fn rename_symbol(cx: &mut Context) { } } else { cx.editor - .set_error("No language server supports symbol renaming"); + .set_error("No configured language server supports symbol renaming"); } }, ) @@ -1459,7 +1461,7 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { Some(future_offset_encoding) => future_offset_encoding, None => { cx.editor - .set_error("No language server supports document-highlight"); + .set_error("No configured language server supports document-highlight"); return; } }; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 8cfc9fd2..9ab2aa4f 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1330,23 +1330,19 @@ fn lsp_workspace_command( return Ok(()); } let doc = doc!(cx.editor); - let id_options = doc + 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)) - }); - - let (language_server_id, options) = match id_options { - Some(id_options) => id_options, - None => { - cx.editor.set_status( - "No active language servers for this document support workspace commands", - ); - return Ok(()); - } + }) + else { + cx.editor.set_status( + "No active language servers for this document support workspace commands", + ); + return Ok(()); }; if args.is_empty() { From 8ee599942a0e5ff6fa1a908ca076785e0d2bd0c7 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 00:51:41 +0100 Subject: [PATCH 24/41] Optimize gutter diagnostics and simplify shown_diagnostics --- helix-view/src/document.rs | 15 +++------------ helix-view/src/gutter.rs | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 27f5d279..bc81567e 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -179,7 +179,7 @@ pub struct Document { version: i32, // should be usize? pub(crate) modified_since_accessed: bool, - diagnostics: Vec<Diagnostic>, + pub(crate) diagnostics: Vec<Diagnostic>, pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>, diff_handle: Option<DiffHandle>, @@ -1605,17 +1605,8 @@ impl Document { pub fn shown_diagnostics(&self) -> impl Iterator<Item = &Diagnostic> + DoubleEndedIterator { self.diagnostics.iter().filter(|d| { - self.language_servers() - .find(|ls| ls.id() == d.language_server_id) - .and_then(|ls| { - let config = self.language_config()?; - let features = config - .language_servers - .iter() - .find(|features| features.name == ls.name())?; - Some(features.has_feature(LanguageServerFeature::Diagnostics)) - }) - == Some(true) + self.language_servers_with_feature(LanguageServerFeature::Diagnostics) + .any(|ls| ls.id() == d.language_server_id) }) } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 78f879c9..8c8abcc3 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,5 +1,7 @@ use std::fmt::Write; +use helix_core::{syntax::LanguageServerFeature, Diagnostic}; + 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.shown_diagnostics().collect::<Vec<_>>(); + let diagnostics = &doc.diagnostics; Box::new( move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { @@ -64,12 +66,20 @@ pub fn diagnostic<'doc>( } 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 on_line_and_is_visible = |d: &&Diagnostic| { + d.line == line + && doc + .language_servers_with_feature(LanguageServerFeature::Diagnostics) + .any(|ls| ls.id() == d.language_server_id) + }; + let after = diagnostics[index..] + .iter() + .take_while(on_line_and_is_visible); let before = diagnostics[..index] .iter() .rev() - .take_while(|d| d.line == line); + .take_while(on_line_and_is_visible); let diagnostics_on_line = after.chain(before); From 451fe528bbc71e2712c02c1ee0d7ac6fcc1a058b Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 01:18:08 +0100 Subject: [PATCH 25/41] Filter out already seen language servers in requests that can be sent to multiple language servers (code-action, completion, symbol pickers) --- helix-term/src/commands.rs | 3 +++ helix-term/src/commands/lsp.rs | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e2a5975b..d602eaa2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4243,9 +4243,12 @@ pub fn completion(cx: &mut Context) { let text = savepoint.text.clone(); let cursor = savepoint.cursor(); + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::Completion) // TODO this should probably already been filtered in something like "language_servers_with_feature" + .filter(|ls| seen_language_servers.insert(ls.id())) .filter_map(|language_server| { let language_server_id = language_server.id(); let offset_encoding = language_server.offset_encoding(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index a1bd291c..6a024bed 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -37,7 +37,12 @@ use crate::{ }; 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, }; impl ui::menu::Item for lsp::Location { @@ -351,8 +356,11 @@ pub fn symbol_picker(cx: &mut Context) { } let doc = doc!(cx.editor); + let mut seen_language_servers = HashSet::new(); + let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) + .filter(|ls| seen_language_servers.insert(ls.id())) .filter_map(|ls| { let request = ls.document_symbols(doc.identifier())?; Some((request, ls.offset_encoding(), doc.identifier())) @@ -413,8 +421,10 @@ pub fn workspace_symbol_picker(cx: &mut Context) { 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())) .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) .map(|(request, offset_encoding)| async move { let json = request.await?; @@ -573,8 +583,11 @@ pub fn code_action(cx: &mut Context) { let selection_range = doc.selection(view.id).primary(); + let mut seen_language_servers = HashSet::new(); + 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(); From 9d089c27c77cb2797a0495b46477dfe348d09a91 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 01:44:44 +0100 Subject: [PATCH 26/41] Fix docgen again --- xtask/src/docgen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index d360b4bd..034d9918 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -97,8 +97,8 @@ pub fn lang_features() -> Result<String, DynError> { } row.push( lc.language_servers - .keys() - .filter_map(|ls| config.language_server.get(ls)) + .iter() + .filter_map(|ls| config.language_server.get(&ls.name)) .map(|s| md_mono(&s.command.clone())) .collect::<Vec<_>>() .join(", "), From ff262084271492bba239dbc2e5788be3c4d5a4e5 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 17:44:04 +0100 Subject: [PATCH 27/41] Filter language servers also by capabilities in `doc.language_servers_with_feature` * Add `helix_lsp::client::Client::supports_feature(&self, LanguageServerFeature)` * Extend `doc.language_servers_with_feature` to use this method as filter as well * Add macro `language_server_with_feature!` to reduce boilerplate for non-mergeable language server requests (like goto-definition) * Refactored most of the `find_map` code to use the either the macro or filter directly via `doc.language_servers_with_feature` --- helix-lsp/src/client.rs | 99 +++++++++- helix-term/src/commands.rs | 149 +++++++------- helix-term/src/commands/lsp.rs | 352 ++++++++++++++++----------------- helix-term/src/ui/mod.rs | 9 +- helix-view/src/document.rs | 51 ++--- 5 files changed, 349 insertions(+), 311 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 65c6954d..b1a73247 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -4,7 +4,7 @@ use crate::{ 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, @@ -276,6 +276,93 @@ impl Client { .expect("language server not yet initialized!") } + #[inline] // TODO inline? + pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool { + let capabilities = match self.capabilities.get() { + Some(capabilities) => capabilities, + None => return false, // not initialized, TODO unwrap/expect instead? + }; + match feature { + LanguageServerFeature::Format => matches!( + capabilities.document_formatting_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::GotoDeclaration => matches!( + capabilities.declaration_provider, + Some( + lsp::DeclarationCapability::Simple(true) + | lsp::DeclarationCapability::RegistrationOptions(_) + | lsp::DeclarationCapability::Options(_), + ) + ), + LanguageServerFeature::GotoDefinition => matches!( + capabilities.definition_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::GotoTypeDefinition => matches!( + capabilities.type_definition_provider, + Some( + lsp::TypeDefinitionProviderCapability::Simple(true) + | lsp::TypeDefinitionProviderCapability::Options(_), + ) + ), + LanguageServerFeature::GotoReference => matches!( + capabilities.references_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::GotoImplementation => matches!( + capabilities.implementation_provider, + Some( + lsp::ImplementationProviderCapability::Simple(true) + | lsp::ImplementationProviderCapability::Options(_), + ) + ), + LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(), + LanguageServerFeature::Hover => matches!( + capabilities.hover_provider, + Some( + lsp::HoverProviderCapability::Simple(true) + | lsp::HoverProviderCapability::Options(_), + ) + ), + LanguageServerFeature::DocumentHighlight => matches!( + capabilities.document_highlight_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::Completion => capabilities.completion_provider.is_some(), + LanguageServerFeature::CodeAction => matches!( + capabilities.code_action_provider, + Some( + lsp::CodeActionProviderCapability::Simple(true) + | lsp::CodeActionProviderCapability::Options(_), + ) + ), + LanguageServerFeature::WorkspaceCommand => { + capabilities.execute_command_provider.is_some() + } + LanguageServerFeature::DocumentSymbols => matches!( + capabilities.document_symbol_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::WorkspaceSymbols => matches!( + capabilities.workspace_symbol_provider, + Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + ), + LanguageServerFeature::Diagnostics => true, // there's no extra server capability + LanguageServerFeature::RenameSymbol => matches!( + capabilities.rename_provider, + Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) + ), + LanguageServerFeature::InlayHints => matches!( + capabilities.inlay_hint_provider, + Some( + lsp::OneOf::Left(true) + | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)) + ) + ), + } + } + pub fn offset_encoding(&self) -> OffsetEncoding { self.capabilities() .position_encoding @@ -1301,21 +1388,13 @@ impl Client { 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; } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d602eaa2..749b0ecf 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3236,10 +3236,8 @@ pub mod insert { let trigger_completion = doc .language_servers_with_feature(LanguageServerFeature::Completion) .any(|ls| { - let capabilities = ls.capabilities(); - // TODO: what if trigger is multiple chars long - matches!(&capabilities.completion_provider, Some(lsp::CompletionOptions { + matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions { trigger_characters: Some(triggers), .. }) if triggers.iter().any(|trigger| trigger.contains(ch))) @@ -3252,51 +3250,39 @@ pub mod insert { } fn signature_help(cx: &mut Context, ch: char) { - use futures_util::FutureExt; use helix_lsp::lsp; // if ch matches signature_help char, trigger - let (view, doc) = current!(cx.editor); - // lsp doesn't tell us when to close the signature help, so we request - // the help information again after common close triggers which should - // return None, which in turn closes the popup. - let close_triggers = &[')', ';', '.']; - // TODO support multiple language servers (not just the first that is found) - let future = doc + let doc = doc_mut!(cx.editor); + // 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) - .find_map(|ls| { - let capabilities = ls.capabilities(); + .next() + else { + return; + }; - match capabilities { - lsp::ServerCapabilities { - signature_help_provider: - Some(lsp::SignatureHelpOptions { - trigger_characters: Some(triggers), - // TODO: retrigger_characters - .. - }), - .. - } if triggers.iter().any(|trigger| trigger.contains(ch)) - || close_triggers.contains(&ch) => - { - let pos = doc.position(view.id, ls.offset_encoding()); - ls.text_document_signature_help(doc.identifier(), pos, None) - } - _ if close_triggers.contains(&ch) => ls.text_document_signature_help( - doc.identifier(), - doc.position(view.id, ls.offset_encoding()), - None, - ), - // TODO: what if trigger is multiple chars long - _ => None, - } - }); + let capabilities = language_server.capabilities(); - if let Some(future) = future { - super::signature_help_impl_with_future( - cx, - future.boxed(), - SignatureHelpInvoked::Automatic, - ) + if let lsp::ServerCapabilities { + signature_help_provider: + Some(lsp::SignatureHelpOptions { + trigger_characters: Some(triggers), + // TODO: retrigger_characters + .. + }), + .. + } = capabilities + { + // TODO: what if trigger is multiple chars long + let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); + // lsp doesn't tell us when to close the signature help, so we request + // the help information again after common close triggers which should + // return None, which in turn closes the popup. + let close_triggers = &[')', ';', '.']; + + if is_trigger || close_triggers.contains(&ch) { + super::signature_help_impl(cx, SignatureHelpInvoked::Automatic); + } } } @@ -3310,7 +3296,7 @@ pub mod insert { Some(transaction) } - use helix_core::{auto_pairs, syntax::LanguageServerFeature}; + use helix_core::auto_pairs; pub fn insert_char(cx: &mut Context, c: char) { let (view, doc) = current_ref!(cx.editor); @@ -4065,39 +4051,44 @@ fn format_selections(cx: &mut Context) { .set_error("format_selections only supports a single selection for now"); return; } - let future_offset_encoding = doc + + // 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_map(|language_server| { - 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 future = language_server.text_document_range_formatting( - doc.identifier(), - range, - lsp::FormattingOptions::default(), - None, - )?; - Some((future, offset_encoding)) - }); - - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No configured language server supports range formatting"); - return; - } + .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 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(future)).unwrap_or_default(); let transaction = @@ -4247,15 +4238,15 @@ pub fn completion(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::Completion) - // TODO this should probably already been filtered in something like "language_servers_with_feature" .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|language_server| { + .map(|language_server| { let language_server_id = language_server.id(); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); - let completion_request = language_server.completion(doc.identifier(), pos, None)?; + let doc_id = doc.identifier(); + let completion_request = language_server.completion(doc_id, pos, None).unwrap(); - Some(async move { + async move { let json = completion_request.await?; let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?; @@ -4277,7 +4268,7 @@ pub fn completion(cx: &mut Context) { .collect(); anyhow::Ok(items) - }) + } }) .collect(); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 6a024bed..15f8d93d 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -45,6 +45,28 @@ use std::{ sync::Arc, }; +/// 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_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(format!( + "No configured language server supports {}", + $feature + )); + return; + } + } + }}; +} + impl ui::menu::Item for lsp::Location { /// Current working directory. type Data = PathBuf; @@ -361,36 +383,38 @@ pub fn symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::DocumentSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|ls| { - let request = ls.document_symbols(doc.identifier())?; - Some((request, ls.offset_encoding(), doc.identifier())) - }) - .map(|(request, offset_encoding, doc_id)| 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 - .into_iter() - .map(|symbol| SymbolInformationItem { - symbol, - offset_encoding, - }) - .collect(), - lsp::DocumentSymbolResponse::Nested(symbols) => { - let mut flat_symbols = Vec::new(); - for symbol in symbols { - nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) + .map(|language_server| { + let request = language_server.document_symbols(doc.identifier()).unwrap(); + let offset_encoding = language_server.offset_encoding(); + let doc_id = doc.identifier(); + + 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 + .into_iter() + .map(|symbol| SymbolInformationItem { + symbol, + offset_encoding, + }) + .collect(), + lsp::DocumentSymbolResponse::Nested(symbols) => { + let mut flat_symbols = Vec::new(); + for symbol in symbols { + nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding) + } + flat_symbols } - flat_symbols - } - }; - Ok(symbols) + }; + Ok(symbols) + } }) .collect(); let current_url = doc.url(); @@ -425,20 +449,24 @@ pub fn workspace_symbol_picker(cx: &mut Context) { let mut futures: FuturesUnordered<_> = doc .language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols) .filter(|ls| seen_language_servers.insert(ls.id())) - .filter_map(|ls| Some((ls.workspace_symbols(pattern.clone())?, ls.offset_encoding()))) - .map(|(request, offset_encoding)| async move { - let json = request.await?; + .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(); + 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) + anyhow::Ok(response) + } }) .collect(); @@ -1043,22 +1071,19 @@ where F: Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, { let (view, doc) = current!(cx.editor); - if let Some((future, offset_encoding)) = - doc.run_on_first_supported_language_server(view.id, feature, |ls, encoding, pos, doc_id| { - Some((request_provider(ls, pos, doc_id)?, encoding)) - }) - { - cx.callback( - future, - move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { - let items = to_locations(response); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); - } else { - cx.editor - .set_error("No configured language server supports {feature}"); - } + + 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 = request_provider(language_server, pos, doc.identifier()).unwrap(); + + cx.callback( + future, + move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| { + let items = to_locations(response); + goto_impl(editor, compositor, items, offset_encoding); + }, + ); } pub fn goto_declaration(cx: &mut Context) { @@ -1096,32 +1121,29 @@ pub fn goto_implementation(cx: &mut Context) { pub fn goto_reference(cx: &mut Context) { let config = cx.editor.config(); let (view, doc) = current!(cx.editor); - if let Some((future, offset_encoding)) = doc.run_on_first_supported_language_server( - view.id, - LanguageServerFeature::GotoReference, - |ls, encoding, pos, doc_id| { - Some(( - ls.goto_reference( - doc_id, - pos, - config.lsp.goto_reference_include_declaration, - None, - )?, - encoding, - )) + + // 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 = language_server + .goto_reference( + doc.identifier(), + pos, + config.lsp.goto_reference_include_declaration, + None, + ) + .unwrap(); + + cx.callback( + future, + move |editor, compositor, response: Option<Vec<lsp::Location>>| { + let items = response.unwrap_or_default(); + goto_impl(editor, compositor, items, offset_encoding); }, - ) { - cx.callback( - future, - move |editor, compositor, response: Option<Vec<lsp::Location>>| { - let items = response.unwrap_or_default(); - goto_impl(editor, compositor, items, offset_encoding); - }, - ); - } else { - cx.editor - .set_error("No configured language server supports goto-reference"); - } + ); } #[derive(PartialEq, Eq, Clone, Copy)] @@ -1145,19 +1167,15 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { language_server.text_document_signature_help(doc.identifier(), pos, None) }); - let future = match future { - Some(future) => future.boxed(), - None => { - // 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; + 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, invoked); + signature_help_impl_with_future(cx, future.boxed(), invoked); } pub fn signature_help_impl_with_future( @@ -1272,22 +1290,14 @@ pub fn signature_help_impl_with_future( pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); + // 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 request = doc - .language_servers_with_feature(LanguageServerFeature::Hover) - .find_map(|language_server| { - let pos = doc.position(view.id, language_server.offset_encoding()); - language_server.text_document_hover(doc.identifier(), pos, None) - }); - - let future = match request { - Some(future) => future, - None => { - cx.editor - .set_error("No configured language server supports 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, @@ -1381,34 +1391,26 @@ pub fn rename_symbol(cx: &mut Context) { return; } let (view, doc) = current!(cx.editor); - let request = doc - .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .find_map(|language_server| { - if let Some(language_server_id) = language_server_id { - if language_server.id() != language_server_id { - return None; - } - } - 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(), - )?; - Some((future, offset_encoding)) - }); - if let Some((future, offset_encoding)) = request { - match block_on(future) { - Ok(edits) => { - let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); - } - Err(err) => cx.editor.set_error(err.to_string()), + let Some(language_server) = doc + .language_servers_with_feature(LanguageServerFeature::RenameSymbol) + .find(|ls| language_server_id.is_none() || Some(ls.id()) == language_server_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(); + + match block_on(future) { + Ok(edits) => { + let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits); } - } else { - cx.editor - .set_error("No configured language server supports symbol renaming"); + Err(err) => cx.editor.set_error(err.to_string()), } }, ) @@ -1417,20 +1419,28 @@ pub fn rename_symbol(cx: &mut Context) { Box::new(prompt) } - let (view, doc) = current!(cx.editor); + let (view, doc) = current_ref!(cx.editor); - let prepare_rename_request = doc + let language_server_with_prepare_rename_support = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = language_server.prepare_rename(doc.identifier(), pos)?; - Some((future, offset_encoding, language_server.id())) + .find(|ls| { + matches!( + ls.capabilities().rename_provider, + Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + .. + })) + ) }); - match prepare_rename_request { - // Language server supports textDocument/prepareRename, use it. - Some((future, offset_encoding, ls_id)) => 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) @@ -1446,38 +1456,23 @@ pub fn rename_symbol(cx: &mut Context) { 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, None); - - 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 future_offset_encoding = doc - .language_servers_with_feature(LanguageServerFeature::DocumentHighlight) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = doc.position(view.id, offset_encoding); - let future = - language_server.text_document_document_highlight(doc.identifier(), pos, None)?; - Some((future, offset_encoding)) - }); - let (future, offset_encoding) = match future_offset_encoding { - Some(future_offset_encoding) => future_offset_encoding, - None => { - cx.editor - .set_error("No configured language server supports document-highlight"); - return; - } - }; + 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 = language_server + .text_document_document_highlight(doc.identifier(), pos, None) + .unwrap(); cx.callback( future, @@ -1532,16 +1527,9 @@ fn compute_inlay_hints_for_view( let view_id = view.id; let doc_id = view.doc; - let mut language_servers = doc.language_servers_with_feature(LanguageServerFeature::InlayHints); - let language_server = language_servers.find(|language_server| { - matches!( - language_server.capabilities().inlay_hint_provider, - Some( - lsp::OneOf::Left(true) - | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)) - ) - ) - })?; + let language_server = doc + .language_servers_with_feature(LanguageServerFeature::InlayHints) + .next()?; let doc_text = doc.text(); let len_lines = doc_text.len_lines(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6f7ed174..ec328ec5 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -394,14 +394,11 @@ pub mod completers { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { let matcher = Matcher::default(); - let options = match doc!(editor) + let Some(options) = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) - { - Some(options) => options, - None => { - return vec![]; - } + else { + return vec![]; }; let mut matches: Vec<_> = options diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index bc81567e..f2f373aa 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -580,7 +580,7 @@ where *mut_ref = f(mem::take(mut_ref)); } -use helix_lsp::{lsp, Client, LanguageServerName, OffsetEncoding}; +use helix_lsp::{lsp, Client, LanguageServerName}; use url::Url; impl Document { @@ -732,21 +732,19 @@ impl Document { let text = self.text.clone(); // finds first language server that supports formatting and then formats - let (offset_encoding, request) = self + let language_server = self .language_servers_with_feature(LanguageServerFeature::Format) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let request = language_server.text_document_formatting( - self.identifier(), - lsp::FormattingOptions { - tab_size: self.tab_width() as u32, - insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), - ..Default::default() - }, - None, - )?; - Some((offset_encoding, request)) - })?; + .next()?; + let offset_encoding = language_server.offset_encoding(); + let request = language_server.text_document_formatting( + self.identifier(), + lsp::FormattingOptions { + tab_size: self.tab_width() as u32, + insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)), + ..Default::default() + }, + None, + )?; let fut = async move { let edits = request.await.unwrap_or_else(|e| { @@ -1445,7 +1443,6 @@ impl Document { self.language_servers.remove(name) } - // TODO filter also based on LSP capabilities? pub fn language_servers_with_feature( &self, feature: LanguageServerFeature, @@ -1453,7 +1450,10 @@ impl Document { 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() && features.has_feature(feature) { + if ls.is_initialized() + && ls.supports_feature(feature) + && features.has_feature(feature) + { Some(ls) } else { None @@ -1466,23 +1466,6 @@ impl Document { self.language_servers().any(|l| l.id() == id) } - pub fn run_on_first_supported_language_server<T, P>( - &self, - view_id: ViewId, - feature: LanguageServerFeature, - request_provider: P, - ) -> Option<T> - where - P: Fn(&Client, OffsetEncoding, lsp::Position, lsp::TextDocumentIdentifier) -> Option<T>, - { - self.language_servers_with_feature(feature) - .find_map(|language_server| { - let offset_encoding = language_server.offset_encoding(); - let pos = self.position(view_id, offset_encoding); - request_provider(language_server, offset_encoding, pos, self.identifier()) - }) - } - pub fn diff_handle(&self) -> Option<&DiffHandle> { self.diff_handle.as_ref() } From 073000e54d862ef542387868c897d4f847fbe18a Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 20 Mar 2023 18:04:02 +0100 Subject: [PATCH 28/41] Maintain language servers TOML array order in `doc.language_servers` --- helix-view/src/document.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index f2f373aa..bd3c465d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1429,13 +1429,17 @@ impl Document { self.version } + /// maintains the order as configured in the language_servers TOML array pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> { - self.language_servers.values().filter_map(|l| { - if l.is_initialized() { - Some(&**l) - } else { - None - } + 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 + } + }) }) } From 93fd79a949f0fcaa97fa30f841d90c6c1c3f68de Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Mon, 27 Mar 2023 21:21:42 +0200 Subject: [PATCH 29/41] Remove offset_encoding in CodeActionOrCommandItem, as it can be retrieved on demand --- helix-term/src/commands/lsp.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 15f8d93d..38ba98d4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -535,7 +535,6 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) { struct CodeActionOrCommandItem { lsp_item: lsp::CodeActionOrCommand, - offset_encoding: OffsetEncoding, language_server_id: usize, } @@ -637,9 +636,9 @@ pub fn code_action(cx: &mut Context) { }; let code_action_request = language_server.code_actions(doc.identifier(), range, code_action_context)?; - Some((code_action_request, offset_encoding, language_server_id)) + Some((code_action_request, language_server_id)) }) - .map(|(request, offset_encoding, ls_id)| async move { + .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 { @@ -695,7 +694,6 @@ pub fn code_action(cx: &mut Context) { .into_iter() .map(|lsp_item| CodeActionOrCommandItem { lsp_item, - offset_encoding, language_server_id: ls_id, }) .collect()) @@ -727,7 +725,11 @@ pub fn code_action(cx: &mut Context) { // always present here let action = action.unwrap(); - let offset_encoding = action.offset_encoding; + let Some(language_server) = editor.language_servers.get_by_id(action.language_server_id) else { + editor.set_error("Language Server disappeared"); + return; + }; + let offset_encoding = language_server.offset_encoding(); match &action.lsp_item { lsp::CodeActionOrCommand::Command(command) => { From 2a21b939c432e4b5a186df780a1e97d20ff53120 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Tue, 28 Mar 2023 04:15:03 +0200 Subject: [PATCH 30/41] Fix crash with filtered diagnostics in gutter (e.g. when diagnostics aren't visible) --- helix-view/src/gutter.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 8c8abcc3..475ec5a3 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -83,16 +83,15 @@ pub fn diagnostic<'doc>( 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 { - Some(Severity::Error) => error, - Some(Severity::Warning) | None => warning, - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - }); + if let Some(diagnostic) = diagnostics_on_line.max_by_key(|d| d.severity) { + write!(out, "●").ok(); + return Some(match diagnostic.severity { + Some(Severity::Error) => error, + Some(Severity::Warning) | None => warning, + Some(Severity::Info) => info, + Some(Severity::Hint) => hint, + }); + } } None }, From 3e4bac1d964cc29939b396a7e1e9ebd7feda5e0a Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Sun, 2 Apr 2023 20:05:26 +0200 Subject: [PATCH 31/41] Fix lsp_restart across multiple different document scopes (language servers weren't restarted, if not of the same scope id), and fix some smaller rebase issues --- helix-lsp/src/lib.rs | 10 +++------- helix-term/src/commands/typed.rs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 6b4bb430..3fd9c2bf 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -691,7 +691,6 @@ impl Registry { .language_servers .iter() .filter_map(|LanguageServerFeatures { name, .. }| { - // #[allow(clippy::map_entry)] if self.inner.contains_key(name) { let client = match self.start_client( name.clone(), @@ -708,7 +707,7 @@ impl Registry { .insert(name.clone(), vec![client.clone()]) .unwrap(); - // TODO what if there are different language servers for different workspaces, + // TODO what if there are multiple instances for different workspaces? // I think the language servers will be stopped without being restarted, which is not intended for old_client in old_clients { tokio::spawn(async move { @@ -745,15 +744,12 @@ impl Registry { .language_servers .iter() .map(|LanguageServerFeatures { name, .. }| { - if let Some(clients) = self.inner.get_mut(name) { - // clients.find( - - if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, client)| { + 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())); } - // return Ok((name.clone(), clients.clone())); } let client = self.start_client( name.clone(), diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 9ab2aa4f..706442e4 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1404,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(), @@ -1417,7 +1416,16 @@ 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(); From 56748509bdb6cb16006ccc06eb15a7ee09028afc Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 18:07:41 +0200 Subject: [PATCH 32/41] Reduce boilerplate by 'use lsp::*' in Client::supports_feature, and remove TODO comment --- helix-lsp/src/client.rs | 52 ++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b1a73247..a3711317 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -276,65 +276,62 @@ impl Client { .expect("language server not yet initialized!") } - #[inline] // TODO inline? + /// Client has to be initialized otherwise this function panics + #[inline] pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool { - let capabilities = match self.capabilities.get() { - Some(capabilities) => capabilities, - None => return false, // not initialized, TODO unwrap/expect instead? - }; + let capabilities = self.capabilities(); + + use lsp::*; match feature { LanguageServerFeature::Format => matches!( capabilities.document_formatting_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::GotoDeclaration => matches!( capabilities.declaration_provider, Some( - lsp::DeclarationCapability::Simple(true) - | lsp::DeclarationCapability::RegistrationOptions(_) - | lsp::DeclarationCapability::Options(_), + DeclarationCapability::Simple(true) + | DeclarationCapability::RegistrationOptions(_) + | DeclarationCapability::Options(_), ) ), LanguageServerFeature::GotoDefinition => matches!( capabilities.definition_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::GotoTypeDefinition => matches!( capabilities.type_definition_provider, Some( - lsp::TypeDefinitionProviderCapability::Simple(true) - | lsp::TypeDefinitionProviderCapability::Options(_), + TypeDefinitionProviderCapability::Simple(true) + | TypeDefinitionProviderCapability::Options(_), ) ), LanguageServerFeature::GotoReference => matches!( capabilities.references_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::GotoImplementation => matches!( capabilities.implementation_provider, Some( - lsp::ImplementationProviderCapability::Simple(true) - | lsp::ImplementationProviderCapability::Options(_), + ImplementationProviderCapability::Simple(true) + | ImplementationProviderCapability::Options(_), ) ), LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(), LanguageServerFeature::Hover => matches!( capabilities.hover_provider, - Some( - lsp::HoverProviderCapability::Simple(true) - | lsp::HoverProviderCapability::Options(_), - ) + Some(HoverProviderCapability::Simple(true) | HoverProviderCapability::Options(_),) ), LanguageServerFeature::DocumentHighlight => matches!( capabilities.document_highlight_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::Completion => capabilities.completion_provider.is_some(), LanguageServerFeature::CodeAction => matches!( capabilities.code_action_provider, Some( - lsp::CodeActionProviderCapability::Simple(true) - | lsp::CodeActionProviderCapability::Options(_), + CodeActionProviderCapability::Simple(true) + | CodeActionProviderCapability::Options(_), ) ), LanguageServerFeature::WorkspaceCommand => { @@ -342,23 +339,20 @@ impl Client { } LanguageServerFeature::DocumentSymbols => matches!( capabilities.document_symbol_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::WorkspaceSymbols => matches!( capabilities.workspace_symbol_provider, - Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_)) + Some(OneOf::Left(true) | OneOf::Right(_)) ), LanguageServerFeature::Diagnostics => true, // there's no extra server capability LanguageServerFeature::RenameSymbol => matches!( capabilities.rename_provider, - Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) + Some(OneOf::Left(true)) | Some(OneOf::Right(_)) ), LanguageServerFeature::InlayHints => matches!( capabilities.inlay_hint_provider, - Some( - lsp::OneOf::Left(true) - | lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(_)) - ) + Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_))) ), } } From dcb07673f8f1f22b7a5f8fd56b8dd74107e3096e Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 18:08:14 +0200 Subject: [PATCH 33/41] Reorder id generation for Clients to stay close to the old behavior --- helix-lsp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 3fd9c2bf..66082503 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -665,8 +665,8 @@ impl Registry { .language_server_configs() .get(&name) .ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?; - self.counter += 1; let id = self.counter; + self.counter += 1; let NewClient(client, incoming) = start_client( id, name, From 521cdec5a1a53c61d38ce5a1df85f82857f59149 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 18:10:49 +0200 Subject: [PATCH 34/41] Remove TODO comment in helix_lsp::Registry::restart and add doc-comment on top of function instead --- helix-lsp/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 66082503..989c5e2f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -680,6 +680,9 @@ impl Registry { 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, @@ -707,8 +710,6 @@ impl Registry { .insert(name.clone(), vec![client.clone()]) .unwrap(); - // TODO what if there are multiple instances for different workspaces? - // I think the language servers will be stopped without being restarted, which is not intended for old_client in old_clients { tokio::spawn(async move { let _ = old_client.force_shutdown().await; From 39b9a4bba2a026844348886c9d9f2026a3d6f658 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 18:50:05 +0200 Subject: [PATCH 35/41] Add function `Editor::language_server_by_id` and refactor/simplify related code, also don't 'crash' in completion menu if language_server somehow disappeared --- helix-term/src/application.rs | 2 +- helix-term/src/commands/lsp.rs | 7 +++-- helix-term/src/ui/completion.rs | 47 ++++++++++++++++----------------- helix-view/src/editor.rs | 7 ++++- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index dbb873e0..40c6d8c6 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -665,7 +665,7 @@ impl Application { macro_rules! language_server { () => { - match self.editor.language_servers.get_by_id(server_id) { + 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); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 38ba98d4..f7d35873 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -301,7 +301,7 @@ fn diag_picker( flat_diag.reserve(diags.len()); for (diag, ls) in diags { - if let Some(ls) = cx.editor.language_servers.get_by_id(ls) { + if let Some(ls) = cx.editor.language_server_by_id(ls) { flat_diag.push(PickerDiagnostic { url: url.clone(), diag, @@ -725,7 +725,7 @@ pub fn code_action(cx: &mut Context) { // always present here let action = action.unwrap(); - let Some(language_server) = editor.language_servers.get_by_id(action.language_server_id) else { + let Some(language_server) = editor.language_server_by_id(action.language_server_id) else { editor.set_error("Language Server disappeared"); return; }; @@ -772,8 +772,7 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: // the command is executed on the server and communicated back // to the client asynchronously using workspace edits let future = match editor - .language_servers - .get_by_id(language_server_id) + .language_server_by_id(language_server_id) .and_then(|language_server| language_server.command(cmd)) { Some(future) => future, diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index e62efdac..28a5157c 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -212,6 +212,23 @@ impl Completion { 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("language server disappeared between completion request and application"); + // 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 => { @@ -236,17 +253,11 @@ impl Completion { // always present here let item = item.unwrap(); - let offset_encoding = editor - .language_servers - .get_by_id(item.language_server_id) - .expect("language server disappeared between completion request and application") - .offset_encoding(); - let transaction = item_to_transaction( doc, view.id, item, - offset_encoding, + language_server!(item).offset_encoding(), trigger_offset, true, replace_mode, @@ -262,11 +273,8 @@ impl Completion { // always present here let mut item = item.unwrap().clone(); - let offset_encoding = editor - .language_servers - .get_by_id(item.language_server_id) - .expect("language server disappeared between completion request and application") - .offset_encoding(); + let language_server = language_server!(item); + let offset_encoding = language_server.offset_encoding(); let language_server = editor .language_servers @@ -401,20 +409,11 @@ impl Completion { Some(item) if !item.resolved => item.clone(), _ => return false, }; - let language_server = match cx - .editor - .language_servers - .get_by_id(current_item.language_server_id) - { - 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, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ca2144fd..afb8d91f 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -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 @@ -1093,6 +1093,11 @@ impl Editor { 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_servers(&mut self, doc_id: DocumentId) -> Option<()> { self.launch_language_servers(doc_id) From 2b746ea6fa6f7040e3ea51e1386bb914333594f4 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 18:56:19 +0200 Subject: [PATCH 36/41] Some minor clarity/cosmetic improvements Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-term/src/commands/lsp.rs | 2 +- helix-view/src/editor.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index f7d35873..948f3484 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1395,7 +1395,7 @@ pub fn rename_symbol(cx: &mut Context) { let Some(language_server) = doc .language_servers_with_feature(LanguageServerFeature::RenameSymbol) - .find(|ls| language_server_id.is_none() || Some(ls.id()) == language_server_id) + .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; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index afb8d91f..280002bd 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1136,8 +1136,9 @@ impl Editor { let doc_language_servers_not_in_registry = doc.language_servers.iter().filter(|(name, doc_ls)| { - !language_servers.contains_key(*name) - || language_servers[*name].id() != doc_ls.id() + language_servers + .get(*name) + .map_or(true, |ls| ls.id() != doc_ls.id()) }); for (_, language_server) in doc_language_servers_not_in_registry { @@ -1145,8 +1146,9 @@ impl Editor { } let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| { - !doc.language_servers.contains_key(*name) - || doc.language_servers[*name].id() != ls.id() + doc.language_servers + .get(*name) + .map_or(true, |doc_ls| ls.id() != doc_ls.id()) }); for (_, language_server) in language_servers_not_in_doc { From 656ee24966c17ed505acc2faded2da505e9c7052 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Wed, 5 Apr 2023 20:03:41 +0200 Subject: [PATCH 37/41] Simplify gutter diagnostics rendering by using partition_point instead of binary search Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-view/src/gutter.rs | 42 ++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 475ec5a3..d11cbe4d 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,6 +1,6 @@ use std::fmt::Write; -use helix_core::{syntax::LanguageServerFeature, Diagnostic}; +use helix_core::syntax::LanguageServerFeature; use crate::{ editor::GutterType, @@ -65,35 +65,27 @@ 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 on_line_and_is_visible = |d: &&Diagnostic| { + let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line); + if first_diag_idx_maybe_on_line == diagnostics.len() { + return None; + } + 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) - }; - let after = diagnostics[index..] - .iter() - .take_while(on_line_and_is_visible); - - let before = diagnostics[..index] - .iter() - .rev() - .take_while(on_line_and_is_visible); - - let diagnostics_on_line = after.chain(before); - - if let Some(diagnostic) = diagnostics_on_line.max_by_key(|d| d.severity) { - write!(out, "●").ok(); - return Some(match diagnostic.severity { - Some(Severity::Error) => error, - Some(Severity::Warning) | None => warning, - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - }); + }); + 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 + }) }, ) } From f45bbf165e225142b7f1d9f68d0ffcc9fabd265d Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 6 Apr 2023 02:37:43 +0200 Subject: [PATCH 38/41] Apply all review suggestions (doc_id -> id, error message, unnecessary if) Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de> --- helix-term/src/ui/completion.rs | 2 +- helix-view/src/editor.rs | 6 +++--- helix-view/src/gutter.rs | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 28a5157c..eaa63e93 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -220,7 +220,7 @@ impl Completion { { Some(ls) => ls, None => { - editor.set_error("language server disappeared between completion request and application"); + 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; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 280002bd..1f27603c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1353,10 +1353,10 @@ impl Editor { } doc.set_version_control_head(self.diff_providers.get_current_head_name(&path)); - let doc_id = self.new_document(doc); - let _ = self.launch_language_servers(doc_id); + let id = self.new_document(doc); + let _ = self.launch_language_servers(id); - doc_id + id }; self.switch(id, action); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index d11cbe4d..a332a8a3 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -66,9 +66,6 @@ pub fn diagnostic<'doc>( } use helix_core::diagnostic::Severity; let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line); - if first_diag_idx_maybe_on_line == diagnostics.len() { - return None; - } let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..] .iter() .take_while(|d| { From b6d0e26814c0e2c40a2ff49b2c3be7c8358d438b Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Fri, 14 Apr 2023 01:12:01 +0200 Subject: [PATCH 39/41] Sort language servers table in languages.toml and rename language server 'R' to 'r' --- languages.toml | 168 +++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 89 deletions(-) diff --git a/languages.toml b/languages.toml index d5f07dcd..21fe917f 100644 --- a/languages.toml +++ b/languages.toml @@ -3,101 +3,79 @@ [language-server] -taplo = { command = "taplo", args = ["lsp", "stdio"] } -elixir-ls = { command = "elixir-ls", config = { elixirLS.dialyzerEnabled = false } } -mint = { command = "mint", args = ["ls"] } -vscode-json-language-server = { command = "vscode-json-language-server", args = ["--stdio"], config = { provideFormatter = true } } -clangd = { command = "clangd" } -crystalline = { command = "crystalline", args = ["--stdio"] } -omnisharp = { command = "OmniSharp", args = [ "--languageserver" ] } -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 } } -pylsp = { command = "pylsp" } -nls = { command = "nls" } -nil = { command = "nil" } -solargraph = { command = "solargraph", args = ["stdio"] } +awk-language-server = { command = "awk-language-server" } bash-language-server = { command = "bash-language-server", args = ["start"] } -intelephense = { command = "intelephense", args = ["--stdio"] } -texlab = { command = "texlab" } -lean = { command = "lean", args = [ "--server" ] } -julia = { command = "julia", timeout = 60, args = [ - "--startup-file=no", - "--history-file=no", - "--quiet", - "-e", - "using LanguageServer; runserver()", -] } -jdtls = { command = "jdtls" } -ocamllsp = { command = "ocamllsp" } - -svelteserver = { command = "svelteserver", args = ["--stdio"] } -vuels = { command = "vls" } -yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] } -haskell-language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] } -zls = { command = "zls" } -swipl = { command = "swipl", args = [ - "-g", "use_module(library(lsp_server))", - "-g", "lsp_server:main", - "-t", "halt", "--", "stdio" -] } -cmake-language-server = { command = "cmake-language-server" } -racket = { command = "racket", args = ["-l", "racket-langserver"] } -dart = { command = "dart", args = ["language-server", "--client-id=helix"] } -metals = { command = "metals", config = { "isHttpEnabled" = true } } -docker-langserver = { command = "docker-langserver", args = ["--stdio"] } -elm-language-server = { command = "elm-language-server" } -rescript-language-server = { command = "rescript-language-server", args = ["--stdio"] } -robotframework_ls = { command = "robotframework_ls" } -erlang-ls = { command = "erlang_ls" } -kotlin-language-server = { command = "kotlin-language-server" } -terraform-ls = { command = "terraform-ls", args = ["serve"] } -solc = { command = "solc", args = ["--lsp"] } -R = { command = "R", args = ["--no-echo", "-e", "languageserver::run()"] } -sourcekit-lsp = { command = "sourcekit-lsp" } -vala-language-server = { command = "vala-language-server" } -svlangserver = { command = "svlangserver", args = [] } -vlang-language-server = { command = "v", args = ["ls"] } -openscad-lsp = { command = "openscad-lsp", args = ["--stdio"] } -perlnavigator = { command = "perlnavigator", args= ["--stdio"] } -prisma-language-server = { command = "prisma-language-server", args = ["--stdio"] } +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" } -wgsl_analyzer = { command = "wgsl_analyzer" } +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"] } -idris2-lsp = { command = "idris2-lsp" } +erlang-ls = { command = "erlang_ls" } +forc = { command = "forc", args = ["lsp"] } fortls = { command = "fortls", args = ["--lowercase_intrinsics"] } gleam = { command = "gleam", args = ["lsp"] } -ols = { command = "ols", args = [] } -dot-language-server = { command = "dot-language-server", args = ["--stdio"] } -cuelsp = { command = "cuelsp" } -slint-lsp = { command = "slint-lsp", args = [] } -awk-language-server = { command = "awk-language-server" } -pasls = { command = "pasls", args = [] } +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"] } -marksman = { command = "marksman", args = ["server"] } +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"] } -bass = { command = "bass", args = ["--lsp"] } -purescript-language-server = { command = "purescript-language-server", args = ["--stdio"] } -serve-d = { command = "serve-d" } -bicep-langserver = { command = "bicep-langserver" } -qmlls = { command = "qmlls" } -cl-lsp = { command = "cl-lsp", args = [ "stdio" ] } -dhall-lsp-server = { command = "dhall-lsp-server" } -forc = { command = "forc", args = ["lsp"] } -cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] } -vhdl_ls = { command = "vhdl_ls", args = [] } -regols = { command = "regols" } +marksman = { command = "marksman", args = ["server"] } +metals = { command = "metals", config = { "isHttpEnabled" = true } } +mint = { command = "mint", args = ["ls"] } +nil = { command = "nil" } nimlangserver = { command = "nimlangserver" } - -[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 +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] @@ -124,6 +102,18 @@ 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"] @@ -1528,7 +1518,7 @@ shebangs = ["r", "R"] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } -language-servers = [ "R" ] +language-servers = [ "r" ] [[grammar]] name = "r" @@ -1542,7 +1532,7 @@ file-types = ["rmd", "Rmd"] roots = [] indent = { tab-width = 2, unit = " " } grammar = "markdown" -language-servers = [ "R" ] +language-servers = [ "r" ] [[language]] name = "swift" From f8fa0d8a10e14272742af907ae0aeaef2411ad93 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Fri, 14 Apr 2023 14:54:04 +0200 Subject: [PATCH 40/41] Clarify language-servers documentation for mergeable LSP features (`diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols`) --- book/src/languages.md | 1 + 1 file changed, 1 insertion(+) diff --git a/book/src/languages.md b/book/src/languages.md index 3f244566..e28ebb5a 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -168,6 +168,7 @@ language-servers = [ { name = "typescript-language-server", except-features = [ 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). From 2a512f7c487f0a707a7eb158e24bd478433bcd91 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger <philipp@mildenberger.me> Date: Thu, 18 May 2023 22:24:09 +0200 Subject: [PATCH 41/41] Rebase cleanup/fixes and use lsp::CompletionItem in item_to_transaction directly --- helix-lsp/src/lib.rs | 2 +- helix-term/src/commands.rs | 2 +- helix-term/src/ui/completion.rs | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 989c5e2f..d053dbf9 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -682,7 +682,7 @@ impl Registry { /// 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] + /// See helix_view::editor::Editor::refresh_language_servers pub fn restart( &mut self, language_config: &LanguageConfiguration, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 749b0ecf..9859f64b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4242,7 +4242,7 @@ pub fn completion(cx: &mut Context) { .map(|language_server| { let language_server_id = language_server.id(); let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos(doc.text(), cursor, 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(); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index eaa63e93..d997e8ae 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -119,7 +119,7 @@ impl Completion { fn item_to_transaction( doc: &Document, view_id: ViewId, - item: &CompletionItem, + item: &lsp::CompletionItem, offset_encoding: OffsetEncoding, trigger_offset: usize, include_placeholder: bool, @@ -130,7 +130,7 @@ impl Completion { 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) => { @@ -153,10 +153,9 @@ impl Completion { (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 @@ -165,9 +164,9 @@ impl Completion { (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) ) { @@ -256,7 +255,7 @@ impl Completion { let transaction = item_to_transaction( doc, view.id, - item, + &item.item, language_server!(item).offset_encoding(), trigger_offset, true, @@ -294,7 +293,7 @@ impl Completion { let transaction = item_to_transaction( doc, view.id, - &item, + &item.item, offset_encoding, trigger_offset, false,