diff --git a/Cargo.lock b/Cargo.lock index ca625dd461..21761f96e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5454,6 +5454,7 @@ dependencies = [ "globset", "gpui", "indoc", + "itertools 0.11.0", "lazy_static", "log", "lsp", diff --git a/assets/settings/default.json b/assets/settings/default.json index c5ee98abf0..2ba2268b43 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -294,6 +294,10 @@ "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. "enable_language_server": true, + // The list of language servers to use (or disable) for all languages. + // + // This is typically customized on a per-language basis. + "language_servers": ["..."], // When to automatically save edited buffers. This setting can // take four values. // diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b513fbb255..85819362b1 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -34,11 +34,13 @@ fuzzy.workspace = true git.workspace = true globset.workspace = true gpui.workspace = true +itertools.workspace = true lazy_static.workspace = true log.workspace = true lsp.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark.workspace = true rand = { workspace = true, optional = true } regex.workspace = true rpc.workspace = true @@ -50,15 +52,14 @@ similar = "1.3" smallvec.workspace = true smol.workspace = true sum_tree.workspace = true +task.workspace = true text.workspace = true theme.workspace = true tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } -pulldown-cmark.workspace = true tree-sitter.workspace = true unicase = "2.6" util.workspace = true -task.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 3672fb1e15..e65b823bc1 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,10 +1,11 @@ //! Provides `language`-related settings. -use crate::{File, Language}; +use crate::{File, Language, LanguageServerName}; use anyhow::Result; use collections::{HashMap, HashSet}; use globset::GlobMatcher; use gpui::AppContext; +use itertools::{Either, Itertools}; use schemars::{ schema::{InstanceType, ObjectValidation, Schema, SchemaObject}, JsonSchema, @@ -92,6 +93,13 @@ pub struct LanguageSettings { pub prettier: HashMap, /// Whether to use language servers to provide code intelligence. pub enable_language_server: bool, + /// The list of language servers to use (or disable) for this language. + /// + /// This array should consist of language server IDs, as well as the following + /// special tokens: + /// - `"!"` - A language server ID prefixed with a `!` will be disabled. + /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. + pub language_servers: Vec>, /// Controls whether Copilot provides suggestion immediately (true) /// or waits for a `copilot::Toggle` (false). pub show_copilot_suggestions: bool, @@ -109,6 +117,53 @@ pub struct LanguageSettings { pub code_actions_on_format: HashMap, } +impl LanguageSettings { + /// A token representing the rest of the available language servers. + const REST_OF_LANGUAGE_SERVERS: &'static str = "..."; + + /// Returns the customized list of language servers from the list of + /// available language servers. + pub fn customized_language_servers( + &self, + available_language_servers: &[LanguageServerName], + ) -> Vec { + Self::resolve_language_servers(&self.language_servers, available_language_servers) + } + + pub(crate) fn resolve_language_servers( + configured_language_servers: &[Arc], + available_language_servers: &[LanguageServerName], + ) -> Vec { + let (disabled_language_servers, enabled_language_servers): (Vec>, Vec>) = + configured_language_servers.iter().partition_map( + |language_server| match language_server.strip_prefix('!') { + Some(disabled) => Either::Left(disabled.into()), + None => Either::Right(language_server.clone()), + }, + ); + + let rest = available_language_servers + .into_iter() + .filter(|&available_language_server| { + !disabled_language_servers.contains(&&available_language_server.0) + && !enabled_language_servers.contains(&&available_language_server.0) + }) + .cloned() + .collect::>(); + + enabled_language_servers + .into_iter() + .flat_map(|language_server| { + if language_server.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { + rest.clone() + } else { + vec![LanguageServerName(language_server.clone())] + } + }) + .collect::>() + } +} + /// The settings for [GitHub Copilot](https://github.com/features/copilot). #[derive(Clone, Debug, Default)] pub struct CopilotSettings { @@ -119,7 +174,7 @@ pub struct CopilotSettings { } /// The settings for all languages. -#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { /// The settings for enabling/disabling features. #[serde(default)] @@ -140,7 +195,7 @@ pub struct AllLanguageSettingsContent { } /// The settings for a particular language. -#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { /// How many columns a tab should occupy. /// @@ -211,6 +266,16 @@ pub struct LanguageSettingsContent { /// Default: true #[serde(default)] pub enable_language_server: Option, + /// The list of language servers to use (or disable) for this language. + /// + /// This array should consist of language server IDs, as well as the following + /// special tokens: + /// - `"!"` - A language server ID prefixed with a `!` will be disabled. + /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. + /// + /// Default: ["..."] + #[serde(default)] + pub language_servers: Option>>, /// Controls whether Copilot provides suggestion immediately (true) /// or waits for a `copilot::Toggle` (false). /// @@ -257,7 +322,7 @@ pub struct CopilotSettingsContent { } /// The settings for enabling/disabling features. -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { /// Whether the GitHub Copilot feature is enabled. @@ -608,6 +673,12 @@ impl settings::Settings for AllLanguageSettings { } fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } + merge(&mut settings.tab_size, src.tab_size); merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); @@ -642,6 +713,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent &mut settings.enable_language_server, src.enable_language_server, ); + merge(&mut settings.language_servers, src.language_servers.clone()); merge( &mut settings.show_copilot_suggestions, src.show_copilot_suggestions, @@ -652,9 +724,70 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.extend_comment_on_newline, ); merge(&mut settings.inlay_hints, src.inlay_hints); - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_resolve_language_servers() { + fn language_server_names(names: &[&str]) -> Vec { + names + .into_iter() + .copied() + .map(|name| LanguageServerName(name.into())) + .collect::>() } + + let available_language_servers = language_server_names(&[ + "typescript-language-server", + "biome", + "deno", + "eslint", + "tailwind", + ]); + + // A value of just `["..."]` is the same as taking all of the available language servers. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[LanguageSettings::REST_OF_LANGUAGE_SERVERS.into()], + &available_language_servers, + ), + available_language_servers + ); + + // Referencing one of the available language servers will change its order. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[ + "biome".into(), + LanguageSettings::REST_OF_LANGUAGE_SERVERS.into(), + "deno".into() + ], + &available_language_servers + ), + language_server_names(&[ + "biome", + "typescript-language-server", + "eslint", + "tailwind", + "deno", + ]) + ); + + // Negating an available language server removes it from the list. + assert_eq!( + LanguageSettings::resolve_language_servers( + &[ + "deno".into(), + "!typescript-language-server".into(), + "!biome".into(), + LanguageSettings::REST_OF_LANGUAGE_SERVERS.into() + ], + &available_language_servers + ), + language_server_names(&["deno", "eslint", "tailwind"]) + ); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ad7b67ffe0..019fca5ca2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3060,7 +3060,20 @@ impl Project { return; } - for adapter in self.languages.clone().lsp_adapters(&language) { + let available_lsp_adapters = self.languages.clone().lsp_adapters(&language); + let available_language_servers = available_lsp_adapters + .iter() + .map(|lsp_adapter| lsp_adapter.name.clone()) + .collect::>(); + + let enabled_language_servers = + settings.customized_language_servers(&available_language_servers); + + for adapter in available_lsp_adapters { + if !enabled_language_servers.contains(&adapter.name) { + continue; + } + self.start_language_server(worktree, adapter.clone(), language.clone(), cx); } }