diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index d5552c5f11..3da8fbe650 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -14,7 +14,7 @@ use gpui::{ use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, - language_settings::{Formatter, FormatterList, LanguageSettings}, + language_settings::{ConfiguredLanguageServer, Formatter, FormatterList, LanguageSettings}, rust_lang, tree_sitter_typescript, }; use node_runtime::NodeRuntime; @@ -180,7 +180,7 @@ async fn test_sharing_an_ssh_remote_project( cx_b.read(|cx| { assert_eq!( LanguageSettings::for_buffer(buffer_b.read(cx), cx).language_servers, - ["override-rust-analyzer".to_string()] + [ConfiguredLanguageServer::new("override-rust-analyzer")] ) }); @@ -1441,7 +1441,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m cx_a.read(|cx| { assert_eq!( LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers, - ["...".to_string()], + [ConfiguredLanguageServer::new("...")], "remote .zed/settings.json must not sync before trust approval" ) }); @@ -1469,7 +1469,7 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m cx_a.read(|cx| { assert_eq!( LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers, - ["override-rust-analyzer".to_string()], + [ConfiguredLanguageServer::new("override-rust-analyzer")], "remote .zed/settings.json should sync after trust approval" ) }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 18c56ef03a..ff3e9bb162 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -39242,7 +39242,9 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) { cx ) .language_servers, - ["...".to_string()], + [language::language_settings::ConfiguredLanguageServer::new( + "..." + )], "local .zed/settings.json must not apply before trust approval" ) }); @@ -39275,7 +39277,9 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) { cx ) .language_servers, - ["override-rust-analyzer".to_string()], + [language::language_settings::ConfiguredLanguageServer::new( + "override-rust-analyzer" + )], "local .zed/settings.json should apply after trust approval" ) }); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0b5c68a4af..b73822723d 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -12,15 +12,15 @@ use ec4rs::{ }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers, SharedString}; -use itertools::{Either, Itertools}; +use itertools::Itertools; use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens}; pub use settings::{ - AutoIndentMode, CompletionSettingsContent, EditPredictionDataCollectionChoice, - EditPredictionPromptFormatContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave, - Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LineEndingSetting, - LspInsertMode, REST_OF_LANGUAGE_SERVERS, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, - WordsCompletionMode, + AutoIndentMode, CompletionSettingsContent, ConfiguredLanguageServer, + EditPredictionDataCollectionChoice, EditPredictionPromptFormatContent, EditPredictionProvider, + EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind, + LanguageSettingsContent, LineEndingSetting, LspInsertMode, REST_OF_LANGUAGE_SERVERS, + RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom}; use shellexpand; @@ -102,7 +102,7 @@ pub struct LanguageSettings { /// 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, + pub language_servers: Vec, /// Controls how semantic tokens from language servers are used for syntax highlighting. pub semantic_tokens: SemanticTokens, /// Controls whether folding ranges from language servers are used instead of @@ -355,18 +355,22 @@ impl LanguageSettings { } pub(crate) fn resolve_language_servers( - configured_language_servers: &[String], + configured_language_servers: &[ConfiguredLanguageServer], 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(LanguageServerName(disabled.to_string().into())), - None => Either::Right(LanguageServerName(language_server.clone().into())), - }, - ); + ) = configured_language_servers + .iter() + .partition_map(|language_server| { + let name = LanguageServerName(language_server.name.clone().into()); + if language_server.disabled { + itertools::Either::Left(name) + } else { + itertools::Either::Right(name) + } + }); let rest = available_language_servers .iter() diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 16633b156d..0d53e3d9b4 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -28,7 +28,7 @@ use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point, - language_settings::{AllLanguageSettings, LanguageSettings}, + language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings}, }; use lsp::{ CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, @@ -557,7 +557,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo AllLanguageSettings::get_global(cx) .language(None, Some(&"Rust".into()), cx) .language_servers, - ["from-local-settings"], + [ConfiguredLanguageServer::new("from-local-settings")], "User language settings should be synchronized with the server settings" ) }); @@ -578,7 +578,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo AllLanguageSettings::get_global(cx) .language(None, Some(&"Rust".into()), cx) .language_servers, - ["from-server-settings".to_string()], + [ConfiguredLanguageServer::new("from-server-settings")], "Server language settings should take precedence over the user settings" ) }); @@ -639,14 +639,14 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo ) .language(None, Some(&"Rust".into()), cx) .language_servers, - ["override-rust-analyzer".to_string()] + [ConfiguredLanguageServer::new("override-rust-analyzer")] ) }); cx.read(|cx| { assert_eq!( LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers, - ["override-rust-analyzer".to_string()] + [ConfiguredLanguageServer::new("override-rust-analyzer")] ) }); } @@ -793,7 +793,10 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext cx.read(|cx| { assert_eq!( LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers, - ["rust-analyzer".to_string(), "fake-analyzer".to_string()] + [ + ConfiguredLanguageServer::new("rust-analyzer"), + ConfiguredLanguageServer::new("fake-analyzer"), + ] ) }); diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index ddbfe53b21..922b5211eb 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::{borrow::Cow, num::NonZeroU32}; use collections::{HashMap, HashSet}; use schemars::JsonSchema; @@ -58,7 +58,7 @@ impl merge_from::MergeFrom for AllLanguageSettingsContent { let globally_disabled_servers = other.defaults.language_servers.as_ref().map(|servers| { servers .iter() - .filter(|entry| entry.starts_with('!')) + .filter(|entry| entry.disabled) .cloned() .collect::>() }); @@ -68,14 +68,13 @@ impl merge_from::MergeFrom for AllLanguageSettingsContent { if let Some(mut language_server_overrides) = language_server_overrides { if let Some(disabled) = &globally_disabled_servers { for disabled_server in disabled { - if let Some(enabled_server) = disabled_server.strip_prefix('!') { - language_server_overrides.retain(|entry| entry != enabled_server); - } + language_server_overrides + .retain(|entry| entry.disabled || entry.name != disabled_server.name); } let insert_before = language_server_overrides .iter() - .position(|entry| entry == REST_OF_LANGUAGE_SERVERS) + .position(|entry| entry.name.as_ref() == REST_OF_LANGUAGE_SERVERS) .unwrap_or(language_server_overrides.len()); for disabled_server in disabled { if !language_server_overrides.contains(disabled_server) { @@ -420,6 +419,63 @@ pub enum SoftWrap { pub const REST_OF_LANGUAGE_SERVERS: &str = "..."; +#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] +#[schemars(with = "String")] +pub struct ConfiguredLanguageServer { + pub name: Arc, + pub disabled: bool, +} + +impl ConfiguredLanguageServer { + const DISABLED_CHAR: char = '!'; + + pub fn new(name: impl Into>) -> Self { + Self { + name: name.into(), + disabled: false, + } + } + + pub fn new_disabled(name: impl Into>) -> Self { + Self { + name: name.into(), + disabled: true, + } + } +} + +impl From<&str> for ConfiguredLanguageServer { + fn from(value: &str) -> Self { + match value.strip_prefix(Self::DISABLED_CHAR) { + Some(name) => Self::new_disabled(name), + None => Self::new(value), + } + } +} + +impl Serialize for ConfiguredLanguageServer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.disabled { + serializer.collect_str(&format_args!("{}{}", Self::DISABLED_CHAR, self.name)) + } else { + serializer.serialize_str(&self.name) + } + } +} + +impl<'de> Deserialize<'de> for ConfiguredLanguageServer { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = Cow::<'de, str>::deserialize(deserializer)?; + Ok(Self::from(value.as_ref())) + } +} + /// The settings for a particular language. #[with_fallible_options] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -511,7 +567,7 @@ pub struct LanguageSettingsContent { /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. /// /// Default: ["..."] - pub language_servers: Option>, + pub language_servers: Option>, /// Controls how semantic tokens from language servers are used for syntax highlighting. /// /// Options: @@ -1282,6 +1338,31 @@ mod test { assert!(matches!(result, ParseStatus::Failed { .. })); } + #[test] + fn test_configured_language_server_serialization() { + let enabled = ConfiguredLanguageServer::new("rust-analyzer"); + let disabled = ConfiguredLanguageServer::new_disabled("rust-analyzer"); + + assert_eq!( + serde_json::to_string(&enabled).expect("enabled server should serialize"), + "\"rust-analyzer\"" + ); + assert_eq!( + serde_json::to_string(&disabled).expect("disabled server should serialize"), + "\"!rust-analyzer\"" + ); + assert_eq!( + serde_json::from_str::("\"rust-analyzer\"") + .expect("enabled server should deserialize"), + enabled + ); + assert_eq!( + serde_json::from_str::("\"!rust-analyzer\"") + .expect("disabled server should deserialize"), + disabled + ); + } + #[test] fn test_language_servers_merge_preserves_per_language_config() { let mut base = AllLanguageSettingsContent { @@ -1328,11 +1409,11 @@ mod test { assert_eq!( ts_servers, &vec![ - "!typescript-language-server".to_string(), - "vtsls".to_string(), - "!eslint".to_string(), - "!tailwindcss-language-server".to_string(), - REST_OF_LANGUAGE_SERVERS.to_string(), + ConfiguredLanguageServer::new_disabled("typescript-language-server"), + ConfiguredLanguageServer::new("vtsls"), + ConfiguredLanguageServer::new_disabled("eslint"), + ConfiguredLanguageServer::new_disabled("tailwindcss-language-server"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), ] ); @@ -1340,9 +1421,9 @@ mod test { assert_eq!( default_servers, &vec![ - "!tailwindcss-language-server".to_string(), - "!eslint".to_string(), - REST_OF_LANGUAGE_SERVERS.to_string(), + ConfiguredLanguageServer::new_disabled("tailwindcss-language-server"), + ConfiguredLanguageServer::new_disabled("eslint"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), ] ); } @@ -1383,9 +1464,9 @@ mod test { base.merge_from(&user); let expected = vec![ - "!typescript-language-server".to_string(), - "!vtsls".to_string(), - REST_OF_LANGUAGE_SERVERS.to_string(), + ConfiguredLanguageServer::new_disabled("typescript-language-server"), + ConfiguredLanguageServer::new_disabled("vtsls"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), ]; assert_eq!( base.languages @@ -1430,7 +1511,10 @@ mod test { let rust_servers = base.languages.0["Rust"].language_servers.as_ref().unwrap(); assert_eq!( rust_servers, - &vec!["!eslint".to_string(), REST_OF_LANGUAGE_SERVERS.to_string(),] + &vec![ + ConfiguredLanguageServer::new_disabled("eslint"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), + ] ); } @@ -1491,9 +1575,9 @@ mod test { assert_eq!( ts_servers, &vec![ - "deno".to_string(), - "!vtsls".to_string(), - REST_OF_LANGUAGE_SERVERS.to_string(), + ConfiguredLanguageServer::new("deno"), + ConfiguredLanguageServer::new_disabled("vtsls"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), ] ); } @@ -1550,8 +1634,8 @@ mod test { assert_eq!( ts_servers, &vec![ - "typescript-language-server".to_string(), - REST_OF_LANGUAGE_SERVERS.to_string(), + ConfiguredLanguageServer::new("typescript-language-server"), + ConfiguredLanguageServer::new(REST_OF_LANGUAGE_SERVERS), ] ); }