Add wrapper type for language servers in settings (#61398)

This is in preparation for making configuration of language servers more
comfortable for users/extensions.

Release Notes:

- N/A
This commit is contained in:
Finn Evers authored and GitHub committed 2026-07-22 21:48:46 +00:00
1 parent cb68afbd2b
commit 7dc8071ac8
5 files changed
+145 -50

No files matched your search

@@ -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"
)
});
+6 -2
View File
@@ -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"
)
});
+18 -14
View File
@@ -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:
/// - `"!<language_server_id>"` - 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<String>,
pub language_servers: Vec<ConfiguredLanguageServer>,
/// 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<LanguageServerName> {
let (disabled_language_servers, enabled_language_servers): (
Vec<LanguageServerName>,
Vec<LanguageServerName>,
) = 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()
@@ -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"),
]
)
});
+108 -24
View File
@@ -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::<Vec<_>>()
});
@@ -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<str>,
pub disabled: bool,
}
impl ConfiguredLanguageServer {
const DISABLED_CHAR: char = '!';
pub fn new(name: impl Into<Arc<str>>) -> Self {
Self {
name: name.into(),
disabled: false,
}
}
pub fn new_disabled(name: impl Into<Arc<str>>) -> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<Vec<String>>,
pub language_servers: Option<Vec<ConfiguredLanguageServer>>,
/// 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::<ConfiguredLanguageServer>("\"rust-analyzer\"")
.expect("enabled server should deserialize"),
enabled
);
assert_eq!(
serde_json::from_str::<ConfiguredLanguageServer>("\"!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),
]
);
}