Settings refactor (#38367)
Co-Authored-By: Ben K <ben@zed.dev> Co-Authored-By: Anthony <anthony@zed.dev> Co-Authored-By: Mikayla <mikayla@zed.dev> Release Notes: - settings: Major internal changes to settings. The primary user-facing effect is that some settings which did not make sense in project settings files are no-longer read from there. (For example the inline blame settings) --------- Co-authored-by: Ben Kunkle <ben@zed.dev> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com> Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
co-authored by
Ben Kunkle
Mikayla Maki
Anthony
parent
0a9023bce0
commit
fcdab160f9
@@ -22,7 +22,7 @@ use rpc::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{SettingsKey, SettingsSources, SettingsStore, SettingsUi};
|
||||
use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi};
|
||||
use util::{ResultExt as _, debug_panic};
|
||||
|
||||
use crate::ProjectEnvironment;
|
||||
@@ -989,47 +989,19 @@ impl ExternalAgentServer for LocalCustomAgent {
|
||||
pub const GEMINI_NAME: &'static str = "gemini";
|
||||
pub const CLAUDE_CODE_NAME: &'static str = "claude";
|
||||
|
||||
#[derive(
|
||||
Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq,
|
||||
)]
|
||||
#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)]
|
||||
#[settings_key(key = "agent_servers")]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<BuiltinAgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct BuiltinAgentServerSettings {
|
||||
/// Absolute path to a binary to be used when launching this agent.
|
||||
///
|
||||
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
|
||||
#[serde(rename = "command")]
|
||||
pub path: Option<PathBuf>,
|
||||
/// If a binary is specified in `command`, it will be passed these arguments.
|
||||
pub args: Option<Vec<String>>,
|
||||
/// If a binary is specified in `command`, it will be passed these environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Whether to skip searching `$PATH` for an agent server binary when
|
||||
/// launching this agent.
|
||||
///
|
||||
/// This has no effect if a `command` is specified. Otherwise, when this is
|
||||
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
|
||||
/// is found, use it for threads with this agent. If no agent binary is
|
||||
/// found on `$PATH`, Zed will automatically install and use its own binary.
|
||||
/// When this is `true`, Zed will not search `$PATH`, and will always use
|
||||
/// its own binary.
|
||||
///
|
||||
/// Default: true
|
||||
pub ignore_system_version: Option<bool>,
|
||||
/// The default mode to use for this agent.
|
||||
///
|
||||
/// Note: Not only all agents support modes.
|
||||
///
|
||||
/// Default: None
|
||||
pub default_mode: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1043,6 +1015,18 @@ impl BuiltinAgentServerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
|
||||
fn from(value: settings::BuiltinAgentServerSettings) -> Self {
|
||||
BuiltinAgentServerSettings {
|
||||
path: value.path,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
ignore_system_version: value.ignore_system_version,
|
||||
default_mode: value.default_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
fn from(value: AgentServerCommand) -> Self {
|
||||
BuiltinAgentServerSettings {
|
||||
@@ -1054,9 +1038,8 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
#[derive(Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct CustomAgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
/// The default mode to use for this agent.
|
||||
///
|
||||
@@ -1066,36 +1049,47 @@ pub struct CustomAgentServerSettings {
|
||||
pub default_mode: Option<String>,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != GEMINI_NAME && name != CLAUDE_CODE_NAME {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
fn from(value: settings::CustomAgentServerSettings) -> Self {
|
||||
CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: value.path,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
},
|
||||
default_mode: value.default_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
let agent_settings = content.agent_servers.clone().unwrap();
|
||||
Self {
|
||||
gemini: agent_settings.gemini.map(Into::into),
|
||||
claude: agent_settings.claude.map(Into::into),
|
||||
custom: agent_settings
|
||||
.custom
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
|
||||
let Some(content) = &content.agent_servers else {
|
||||
return;
|
||||
};
|
||||
if let Some(gemini) = content.gemini.clone() {
|
||||
self.gemini = Some(gemini.into())
|
||||
};
|
||||
if let Some(claude) = content.claude.clone() {
|
||||
self.claude = Some(claude.into());
|
||||
}
|
||||
for (name, config) in content.custom.clone() {
|
||||
self.custom.insert(name, config.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
|
||||
}
|
||||
|
||||
@@ -924,7 +924,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -943,7 +943,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -970,7 +970,7 @@ mod tests {
|
||||
vec![
|
||||
(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -979,7 +979,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
server_2_id.0.clone(),
|
||||
ContextServerSettings::Custom {
|
||||
settings::ContextServerSettingsContent::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".into(),
|
||||
@@ -1011,7 +1011,7 @@ mod tests {
|
||||
vec![
|
||||
(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -1020,7 +1020,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
server_2_id.0.clone(),
|
||||
ContextServerSettings::Custom {
|
||||
settings::ContextServerSettingsContent::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".into(),
|
||||
@@ -1047,7 +1047,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -1070,7 +1070,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Extension {
|
||||
settings::ContextServerSettingsContent::Extension {
|
||||
enabled: true,
|
||||
settings: json!({
|
||||
"somevalue": false
|
||||
@@ -1156,7 +1156,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Custom {
|
||||
settings::ContextServerSettingsContent::Custom {
|
||||
enabled: false,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".into(),
|
||||
@@ -1185,7 +1185,7 @@ mod tests {
|
||||
set_context_server_configuration(
|
||||
vec![(
|
||||
server_1_id.0.clone(),
|
||||
ContextServerSettings::Custom {
|
||||
settings::ContextServerSettingsContent::Custom {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "somebinary".into(),
|
||||
@@ -1203,18 +1203,17 @@ mod tests {
|
||||
}
|
||||
|
||||
fn set_context_server_configuration(
|
||||
context_servers: Vec<(Arc<str>, ContextServerSettings)>,
|
||||
context_servers: Vec<(Arc<str>, settings::ContextServerSettingsContent)>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
let mut settings = ProjectSettings::default();
|
||||
for (id, config) in context_servers {
|
||||
settings.context_servers.insert(id, config);
|
||||
}
|
||||
store
|
||||
.set_user_settings(&serde_json::to_string(&settings).unwrap(), cx)
|
||||
.unwrap();
|
||||
store.update_user_settings(cx, |content| {
|
||||
content.project.context_servers.clear();
|
||||
for (id, config) in context_servers {
|
||||
content.project.context_servers.insert(id, config);
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ use super::{
|
||||
session::{self, Session, SessionStateEvent},
|
||||
};
|
||||
use crate::{
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings, worktree_store::WorktreeStore,
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
|
||||
debugger::session::SessionQuirks,
|
||||
project_settings::{DapBinary, ProjectSettings},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -28,8 +30,9 @@ use futures::{
|
||||
};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use http_client::HttpClient;
|
||||
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
||||
use language::{Buffer, LanguageToolchainStore};
|
||||
use node_runtime::NodeRuntime;
|
||||
use settings::InlayHintKind;
|
||||
|
||||
use remote::RemoteClient;
|
||||
use rpc::{
|
||||
@@ -208,8 +211,10 @@ impl DapStore {
|
||||
let dap_settings = ProjectSettings::get(Some(settings_location), cx)
|
||||
.dap
|
||||
.get(&adapter.name());
|
||||
let user_installed_path =
|
||||
dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from));
|
||||
let user_installed_path = dap_settings.and_then(|s| match &s.binary {
|
||||
DapBinary::Default => None,
|
||||
DapBinary::Custom(binary) => Some(PathBuf::from(binary)),
|
||||
});
|
||||
let user_args = dap_settings.map(|s| s.args.clone());
|
||||
|
||||
let delegate = self.delegate(worktree, console, cx);
|
||||
|
||||
@@ -5816,7 +5816,8 @@ impl LspStore {
|
||||
buffer.read(cx).file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
.completions
|
||||
.clone();
|
||||
if !completion_settings.lsp {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
+48
-172
@@ -29,7 +29,6 @@ use context_server_store::ContextServerStore;
|
||||
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
|
||||
use git::repository::get_git_committer;
|
||||
use git_store::{Repository, RepositoryId};
|
||||
use schemars::JsonSchema;
|
||||
pub mod search_history;
|
||||
mod yarn;
|
||||
|
||||
@@ -101,10 +100,7 @@ use rpc::{
|
||||
};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
use settings::{
|
||||
InvalidSettingsError, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore,
|
||||
SettingsUi,
|
||||
};
|
||||
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
|
||||
use smol::channel::Receiver;
|
||||
use snippet::Snippet;
|
||||
use snippet_provider::SnippetProvider;
|
||||
@@ -980,46 +976,23 @@ pub struct DisableAiSettings {
|
||||
pub disable_ai: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Debug,
|
||||
Default,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
SettingsUi,
|
||||
SettingsKey,
|
||||
JsonSchema,
|
||||
)]
|
||||
#[settings_key(None)]
|
||||
pub struct DisableAiSettingContent {
|
||||
pub disable_ai: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Settings for DisableAiSettings {
|
||||
type FileContent = DisableAiSettingContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
// For security reasons, settings can only make AI restrictions MORE strict, not less.
|
||||
// (For example, if someone is working on a project that contractually
|
||||
// requires no AI use, that should override the user's setting which
|
||||
// permits AI use.)
|
||||
// This also prevents an attacker from using project or server settings to enable AI when it should be disabled.
|
||||
let disable_ai = sources
|
||||
.project
|
||||
.iter()
|
||||
.chain(sources.user.iter())
|
||||
.chain(sources.server.iter())
|
||||
.chain(sources.release_channel.iter())
|
||||
.chain(sources.profile.iter())
|
||||
.any(|disabled| disabled.disable_ai == Some(true));
|
||||
|
||||
Ok(Self { disable_ai })
|
||||
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
Self {
|
||||
disable_ai: content.disable_ai.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
|
||||
// If disable_ai is true *in any file*, it is disabled.
|
||||
self.disable_ai = self.disable_ai || content.disable_ai.unwrap_or(false);
|
||||
}
|
||||
|
||||
fn import_from_vscode(
|
||||
_vscode: &settings::VsCodeSettings,
|
||||
_current: &mut settings::SettingsContent,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
@@ -3271,7 +3244,6 @@ impl Project {
|
||||
) {
|
||||
self.buffers_needing_diff.insert(buffer.downgrade());
|
||||
let first_insertion = self.buffers_needing_diff.len() == 1;
|
||||
|
||||
let settings = ProjectSettings::get_global(cx);
|
||||
let delay = if let Some(delay) = settings.git.gutter_debounce {
|
||||
delay
|
||||
@@ -5668,146 +5640,50 @@ fn provide_inline_values(
|
||||
mod disable_ai_settings_tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::Settings;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
|
||||
fn disable_setting(value: Option<bool>) -> DisableAiSettingContent {
|
||||
DisableAiSettingContent { disable_ai: value }
|
||||
}
|
||||
cx.update(|cx| {
|
||||
// Test 1: Default is false (AI enabled)
|
||||
let sources = SettingsSources {
|
||||
default: &DisableAiSettingContent {
|
||||
disable_ai: Some(false),
|
||||
},
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: None,
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: None,
|
||||
project: &[],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
assert!(!settings.disable_ai, "Default should allow AI");
|
||||
settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
// Test 2: Global true, local false -> still disabled (local cannot re-enable)
|
||||
let global_true = disable_setting(Some(true));
|
||||
let local_false = disable_setting(Some(false));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&global_true),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: None,
|
||||
project: &[&local_false],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
// Test 1: Default is false (AI enabled)
|
||||
assert!(
|
||||
settings.disable_ai,
|
||||
!DisableAiSettings::get_global(cx).disable_ai,
|
||||
"Default should allow AI"
|
||||
);
|
||||
});
|
||||
|
||||
let disable_true = serde_json::json!({
|
||||
"disable_ai": true
|
||||
})
|
||||
.to_string();
|
||||
let disable_false = serde_json::json!({
|
||||
"disable_ai": false
|
||||
})
|
||||
.to_string();
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.set_user_settings(&disable_false, cx).unwrap();
|
||||
store.set_global_settings(&disable_true, cx).unwrap();
|
||||
});
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
DisableAiSettings::get_global(cx).disable_ai,
|
||||
"Local false cannot override global true"
|
||||
);
|
||||
});
|
||||
|
||||
// Test 3: Global false, local true -> disabled (local can make more restrictive)
|
||||
let global_false = disable_setting(Some(false));
|
||||
let local_true = disable_setting(Some(true));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&global_false),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: None,
|
||||
project: &[&local_true],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
assert!(settings.disable_ai, "Local true can override global false");
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.set_global_settings(&disable_false, cx).unwrap();
|
||||
store.set_user_settings(&disable_true, cx).unwrap();
|
||||
});
|
||||
|
||||
// Test 4: Server can only make more restrictive (set to true)
|
||||
let user_false = disable_setting(Some(false));
|
||||
let server_true = disable_setting(Some(true));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&user_false),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: Some(&server_true),
|
||||
project: &[],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
cx.update(|cx| {
|
||||
assert!(
|
||||
settings.disable_ai,
|
||||
"Server can set to true even if user is false"
|
||||
);
|
||||
|
||||
// Test 5: Server false cannot override user true
|
||||
let user_true = disable_setting(Some(true));
|
||||
let server_false = disable_setting(Some(false));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&user_true),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: Some(&server_false),
|
||||
project: &[],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
assert!(
|
||||
settings.disable_ai,
|
||||
"Server false cannot override user true"
|
||||
);
|
||||
|
||||
// Test 6: Multiple local settings, any true disables AI
|
||||
let global_false = disable_setting(Some(false));
|
||||
let local_false3 = disable_setting(Some(false));
|
||||
let local_true2 = disable_setting(Some(true));
|
||||
let local_false4 = disable_setting(Some(false));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&global_false),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: None,
|
||||
project: &[&local_false3, &local_true2, &local_false4],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
assert!(settings.disable_ai, "Any local true should disable AI");
|
||||
|
||||
// Test 7: All three sources can independently disable AI
|
||||
let user_false2 = disable_setting(Some(false));
|
||||
let server_false2 = disable_setting(Some(false));
|
||||
let local_true3 = disable_setting(Some(true));
|
||||
let sources = SettingsSources {
|
||||
default: &disable_setting(Some(false)),
|
||||
global: None,
|
||||
extensions: None,
|
||||
user: Some(&user_false2),
|
||||
release_channel: None,
|
||||
operating_system: None,
|
||||
profile: None,
|
||||
server: Some(&server_false2),
|
||||
project: &[&local_true3],
|
||||
};
|
||||
let settings = DisableAiSettings::load(sources, cx).unwrap();
|
||||
assert!(
|
||||
settings.disable_ai,
|
||||
"Local can disable even if user and server are false"
|
||||
DisableAiSettings::get_global(cx).disable_ai,
|
||||
"Local false cannot override global true"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,18 +17,19 @@ use rpc::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use settings::DirenvSettings;
|
||||
pub use settings::LspSettings;
|
||||
use settings::{
|
||||
InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
|
||||
SettingsSources, SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
|
||||
DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
|
||||
SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
|
||||
use util::{ResultExt, serde::default_true};
|
||||
use util::{MergeFrom as _, ResultExt, serde::default_true};
|
||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
||||
|
||||
use crate::{
|
||||
@@ -36,8 +37,7 @@ use crate::{
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(None)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectSettings {
|
||||
/// Configuration for language servers.
|
||||
///
|
||||
@@ -47,48 +47,75 @@ pub struct ProjectSettings {
|
||||
/// To override settings for a language, add an entry for that language server's
|
||||
/// name to the lsp value.
|
||||
/// Default: null
|
||||
#[serde(default)]
|
||||
pub lsp: HashMap<LanguageServerName, LspSettings>,
|
||||
// todo(settings-follow-up)
|
||||
// We should change to use a non content type (settings::LspSettings is a content type)
|
||||
// Note: Will either require merging with defaults, which also requires deciding where the defaults come from,
|
||||
// or case by case deciding which fields are optional and which are actually required.
|
||||
pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
|
||||
|
||||
/// Common language server settings.
|
||||
#[serde(default)]
|
||||
pub global_lsp_settings: GlobalLspSettings,
|
||||
|
||||
/// Configuration for Debugger-related features
|
||||
#[serde(default)]
|
||||
pub dap: HashMap<DebugAdapterName, DapSettings>,
|
||||
|
||||
/// Settings for context servers used for AI-related features.
|
||||
#[serde(default)]
|
||||
pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
|
||||
|
||||
/// Configuration for Diagnostics-related features.
|
||||
#[serde(default)]
|
||||
pub diagnostics: DiagnosticsSettings,
|
||||
|
||||
/// Configuration for Git-related features
|
||||
#[serde(default)]
|
||||
pub git: GitSettings,
|
||||
|
||||
/// Configuration for Node-related features
|
||||
#[serde(default)]
|
||||
pub node: NodeBinarySettings,
|
||||
|
||||
/// Configuration for how direnv configuration should be loaded
|
||||
#[serde(default)]
|
||||
pub load_direnv: DirenvSettings,
|
||||
|
||||
/// Configuration for session-related features
|
||||
#[serde(default)]
|
||||
pub session: SessionSettings,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SessionSettings {
|
||||
/// Whether or not to restore unsaved buffers on restart.
|
||||
///
|
||||
/// If this is true, user won't be prompted whether to save/discard
|
||||
/// dirty files when closing the application.
|
||||
///
|
||||
/// Default: true
|
||||
pub restore_unsaved_buffers: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DapSettings {
|
||||
pub binary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub struct NodeBinarySettings {
|
||||
/// The path to the Node binary.
|
||||
pub path: Option<String>,
|
||||
/// The path to the npm binary Zed should use (defaults to `.path/../npm`).
|
||||
pub npm_path: Option<String>,
|
||||
/// If enabled, Zed will download its own copy of Node.
|
||||
pub ignore_system_version: bool,
|
||||
}
|
||||
|
||||
impl From<settings::NodeBinarySettings> for NodeBinarySettings {
|
||||
fn from(settings: settings::NodeBinarySettings) -> Self {
|
||||
Self {
|
||||
path: settings.path,
|
||||
npm_path: settings.npm_path,
|
||||
ignore_system_version: settings.ignore_system_version.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common language server settings.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GlobalLspSettings {
|
||||
/// Whether to show the LSP servers button in the status bar.
|
||||
///
|
||||
/// Default: `true`
|
||||
pub button: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||
@@ -114,14 +141,29 @@ pub enum ContextServerSettings {
|
||||
},
|
||||
}
|
||||
|
||||
/// Common language server settings.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GlobalLspSettings {
|
||||
/// Whether to show the LSP servers button in the status bar.
|
||||
///
|
||||
/// Default: `true`
|
||||
#[serde(default = "default_true")]
|
||||
pub button: bool,
|
||||
impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
|
||||
fn from(value: settings::ContextServerSettingsContent) -> Self {
|
||||
match value {
|
||||
settings::ContextServerSettingsContent::Custom { enabled, command } => {
|
||||
ContextServerSettings::Custom { enabled, command }
|
||||
}
|
||||
settings::ContextServerSettingsContent::Extension { enabled, settings } => {
|
||||
ContextServerSettings::Extension { enabled, settings }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
|
||||
fn into(self) -> settings::ContextServerSettingsContent {
|
||||
match self {
|
||||
ContextServerSettings::Custom { enabled, command } => {
|
||||
settings::ContextServerSettingsContent::Custom { enabled, command }
|
||||
}
|
||||
ContextServerSettings::Extension { enabled, settings } => {
|
||||
settings::ContextServerSettingsContent::Extension { enabled, settings }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextServerSettings {
|
||||
@@ -147,140 +189,6 @@ impl ContextServerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NodeBinarySettings {
|
||||
/// The path to the Node binary.
|
||||
pub path: Option<String>,
|
||||
/// The path to the npm binary Zed should use (defaults to `.path/../npm`).
|
||||
pub npm_path: Option<String>,
|
||||
/// If enabled, Zed will download its own copy of Node.
|
||||
#[serde(default)]
|
||||
pub ignore_system_version: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DirenvSettings {
|
||||
/// Load direnv configuration through a shell hook
|
||||
ShellHook,
|
||||
/// Load direnv configuration directly using `direnv export json`
|
||||
#[default]
|
||||
Direct,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct DiagnosticsSettings {
|
||||
/// Whether to show the project diagnostics button in the status bar.
|
||||
pub button: bool,
|
||||
|
||||
/// Whether or not to include warning diagnostics.
|
||||
pub include_warnings: bool,
|
||||
|
||||
/// Settings for using LSP pull diagnostics mechanism in Zed.
|
||||
pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
|
||||
|
||||
/// Settings for showing inline diagnostics.
|
||||
pub inline: InlineDiagnosticsSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct LspPullDiagnosticsSettings {
|
||||
/// Whether to pull for diagnostics or not.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Minimum time to wait before pulling diagnostics from the language server(s).
|
||||
/// 0 turns the debounce off.
|
||||
///
|
||||
/// Default: 50
|
||||
#[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
|
||||
pub debounce_ms: u64,
|
||||
}
|
||||
|
||||
fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
|
||||
50
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct InlineDiagnosticsSettings {
|
||||
/// Whether or not to show inline diagnostics
|
||||
///
|
||||
/// Default: false
|
||||
pub enabled: bool,
|
||||
/// Whether to only show the inline diagnostics after a delay after the
|
||||
/// last editor event.
|
||||
///
|
||||
/// Default: 150
|
||||
#[serde(default = "default_inline_diagnostics_update_debounce_ms")]
|
||||
pub update_debounce_ms: u64,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline diagnostic in units of columns.
|
||||
///
|
||||
/// Default: 4
|
||||
#[serde(default = "default_inline_diagnostics_padding")]
|
||||
pub padding: u32,
|
||||
/// The minimum column to display inline diagnostics. This setting can be
|
||||
/// used to horizontally align inline diagnostics at some position. Lines
|
||||
/// longer than this value will still push diagnostics further to the right.
|
||||
///
|
||||
/// Default: 0
|
||||
pub min_column: u32,
|
||||
|
||||
pub max_severity: Option<DiagnosticSeverity>,
|
||||
}
|
||||
|
||||
fn default_inline_diagnostics_update_debounce_ms() -> u64 {
|
||||
150
|
||||
}
|
||||
|
||||
fn default_inline_diagnostics_padding() -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
impl Default for DiagnosticsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button: true,
|
||||
include_warnings: true,
|
||||
lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
|
||||
inline: InlineDiagnosticsSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LspPullDiagnosticsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InlineDiagnosticsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
|
||||
padding: default_inline_diagnostics_padding(),
|
||||
min_column: 0,
|
||||
max_severity: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GlobalLspSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button: default_true(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
@@ -301,7 +209,6 @@ pub enum DiagnosticSeverity {
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
#[serde(alias = "all")]
|
||||
Hint,
|
||||
}
|
||||
|
||||
@@ -317,6 +224,18 @@ impl DiagnosticSeverity {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
|
||||
fn from(severity: settings::DiagnosticSeverityContent) -> Self {
|
||||
match severity {
|
||||
settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
|
||||
settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
|
||||
settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
|
||||
settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
|
||||
settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the severity of the diagnostic that should be moved to.
|
||||
#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -390,12 +309,12 @@ impl GoToDiagnosticSeverityFilter {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct GitSettings {
|
||||
/// Whether or not to show the git gutter.
|
||||
///
|
||||
/// Default: tracked_files
|
||||
pub git_gutter: Option<GitGutterSetting>,
|
||||
pub git_gutter: settings::GitGutterSetting,
|
||||
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
|
||||
///
|
||||
/// Default: null
|
||||
@@ -404,111 +323,50 @@ pub struct GitSettings {
|
||||
/// the currently focused line.
|
||||
///
|
||||
/// Default: on
|
||||
pub inline_blame: Option<InlineBlameSettings>,
|
||||
pub inline_blame: InlineBlameSettings,
|
||||
/// Which information to show in the branch picker.
|
||||
///
|
||||
/// Default: on
|
||||
pub branch_picker: Option<BranchPickerSettings>,
|
||||
pub branch_picker: BranchPickerSettings,
|
||||
/// How hunks are displayed visually in the editor.
|
||||
///
|
||||
/// Default: staged_hollow
|
||||
pub hunk_style: Option<GitHunkStyleSetting>,
|
||||
pub hunk_style: settings::GitHunkStyleSetting,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
pub fn inline_blame_enabled(&self) -> bool {
|
||||
#[allow(unknown_lints, clippy::manual_unwrap_or_default)]
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings { enabled, .. }) => enabled,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_blame_delay(&self) -> Option<Duration> {
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
|
||||
Some(Duration::from_millis(delay_ms))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_inline_commit_summary(&self) -> bool {
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings {
|
||||
show_commit_summary,
|
||||
..
|
||||
}) => show_commit_summary,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitHunkStyleSetting {
|
||||
/// Show unstaged hunks with a filled background and staged hunks hollow.
|
||||
#[default]
|
||||
StagedHollow,
|
||||
/// Show unstaged hunks hollow and staged hunks with a filled background.
|
||||
UnstagedHollow,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitGutterSetting {
|
||||
/// Show git gutter in tracked files.
|
||||
#[default]
|
||||
TrackedFiles,
|
||||
/// Hide git gutter
|
||||
Hide,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct InlineBlameSettings {
|
||||
/// Whether or not to show git blame data inline in
|
||||
/// the currently focused line.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// Whether to only show the inline blame information
|
||||
/// after a delay once the cursor stops moving.
|
||||
///
|
||||
/// Default: 0
|
||||
#[serde(default)]
|
||||
pub delay_ms: u64,
|
||||
pub delay_ms: std::time::Duration,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline blame in units of columns.
|
||||
///
|
||||
/// Default: 7
|
||||
#[serde(default = "default_inline_blame_padding")]
|
||||
pub padding: u32,
|
||||
/// The minimum column number to show the inline blame information at
|
||||
///
|
||||
/// Default: 0
|
||||
#[serde(default)]
|
||||
pub min_column: u32,
|
||||
/// Whether to show commit summary as part of the inline blame.
|
||||
///
|
||||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub show_commit_summary: bool,
|
||||
}
|
||||
|
||||
fn default_inline_blame_padding() -> u32 {
|
||||
7
|
||||
}
|
||||
|
||||
impl Default for InlineBlameSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
delay_ms: 0,
|
||||
padding: default_inline_blame_padding(),
|
||||
min_column: 0,
|
||||
show_commit_summary: false,
|
||||
impl GitSettings {
|
||||
pub fn inline_blame_delay(&self) -> Option<Duration> {
|
||||
if self.inline_blame.delay_ms.as_millis() > 0 {
|
||||
Some(self.inline_blame.delay_ms)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,93 +389,273 @@ impl Default for BranchPickerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
pub struct BinarySettings {
|
||||
pub path: Option<String>,
|
||||
pub arguments: Option<Vec<String>>,
|
||||
pub env: Option<BTreeMap<String, String>>,
|
||||
pub ignore_system_version: Option<bool>,
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiagnosticsSettings {
|
||||
/// Whether to show the project diagnostics button in the status bar.
|
||||
pub button: bool,
|
||||
|
||||
/// Whether or not to include warning diagnostics.
|
||||
pub include_warnings: bool,
|
||||
|
||||
/// Settings for using LSP pull diagnostics mechanism in Zed.
|
||||
pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
|
||||
|
||||
/// Settings for showing inline diagnostics.
|
||||
pub inline: InlineDiagnosticsSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
pub struct FetchSettings {
|
||||
// Whether to consider pre-releases for fetching
|
||||
pub pre_release: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct LspSettings {
|
||||
pub binary: Option<BinarySettings>,
|
||||
pub initialization_options: Option<serde_json::Value>,
|
||||
pub settings: Option<serde_json::Value>,
|
||||
/// If the server supports sending tasks over LSP extensions,
|
||||
/// this setting can be used to enable or disable them in Zed.
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_lsp_tasks: bool,
|
||||
pub fetch: Option<FetchSettings>,
|
||||
}
|
||||
|
||||
impl Default for LspSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
binary: None,
|
||||
initialization_options: None,
|
||||
settings: None,
|
||||
enable_lsp_tasks: true,
|
||||
fetch: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SessionSettings {
|
||||
/// Whether or not to restore unsaved buffers on restart.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InlineDiagnosticsSettings {
|
||||
/// Whether or not to show inline diagnostics
|
||||
///
|
||||
/// If this is true, user won't be prompted whether to save/discard
|
||||
/// dirty files when closing the application.
|
||||
/// Default: false
|
||||
pub enabled: bool,
|
||||
/// Whether to only show the inline diagnostics after a delay after the
|
||||
/// last editor event.
|
||||
///
|
||||
/// Default: 150
|
||||
pub update_debounce_ms: u64,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline diagnostic in units of columns.
|
||||
///
|
||||
/// Default: 4
|
||||
pub padding: u32,
|
||||
/// The minimum column to display inline diagnostics. This setting can be
|
||||
/// used to horizontally align inline diagnostics at some position. Lines
|
||||
/// longer than this value will still push diagnostics further to the right.
|
||||
///
|
||||
/// Default: 0
|
||||
pub min_column: u32,
|
||||
|
||||
pub max_severity: Option<DiagnosticSeverity>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct LspPullDiagnosticsSettings {
|
||||
/// Whether to pull for diagnostics or not.
|
||||
///
|
||||
/// Default: true
|
||||
pub restore_unsaved_buffers: bool,
|
||||
}
|
||||
|
||||
impl Default for SessionSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
restore_unsaved_buffers: true,
|
||||
}
|
||||
}
|
||||
pub enabled: bool,
|
||||
/// Minimum time to wait before pulling diagnostics from the language server(s).
|
||||
/// 0 turns the debounce off.
|
||||
///
|
||||
/// Default: 50
|
||||
pub debounce_ms: u64,
|
||||
}
|
||||
|
||||
impl Settings for ProjectSettings {
|
||||
type FileContent = Self;
|
||||
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
|
||||
let project = &content.project.clone();
|
||||
let diagnostics = content.diagnostics.as_ref().unwrap();
|
||||
let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
|
||||
let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
let git = content.git.as_ref().unwrap();
|
||||
let git_settings = GitSettings {
|
||||
git_gutter: git.git_gutter.unwrap(),
|
||||
gutter_debounce: git.gutter_debounce,
|
||||
inline_blame: {
|
||||
let inline = git.inline_blame.unwrap();
|
||||
InlineBlameSettings {
|
||||
enabled: inline.enabled.unwrap(),
|
||||
delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
|
||||
padding: inline.padding.unwrap(),
|
||||
min_column: inline.min_column.unwrap(),
|
||||
show_commit_summary: inline.show_commit_summary.unwrap(),
|
||||
}
|
||||
},
|
||||
branch_picker: {
|
||||
let branch_picker = git.branch_picker.unwrap();
|
||||
BranchPickerSettings {
|
||||
show_author_name: branch_picker.show_author_name.unwrap(),
|
||||
}
|
||||
},
|
||||
hunk_style: git.hunk_style.unwrap(),
|
||||
};
|
||||
Self {
|
||||
context_servers: project
|
||||
.context_servers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into()))
|
||||
.collect(),
|
||||
lsp: project
|
||||
.lsp
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (LanguageServerName(key.into()), value))
|
||||
.collect(),
|
||||
global_lsp_settings: GlobalLspSettings {
|
||||
button: content
|
||||
.global_lsp_settings
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.button
|
||||
.unwrap(),
|
||||
},
|
||||
dap: project
|
||||
.dap
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
|
||||
.collect(),
|
||||
diagnostics: DiagnosticsSettings {
|
||||
button: diagnostics.button.unwrap(),
|
||||
include_warnings: diagnostics.include_warnings.unwrap(),
|
||||
lsp_pull_diagnostics: LspPullDiagnosticsSettings {
|
||||
enabled: lsp_pull_diagnostics.enabled.unwrap(),
|
||||
debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
|
||||
},
|
||||
inline: InlineDiagnosticsSettings {
|
||||
enabled: inline_diagnostics.enabled.unwrap(),
|
||||
update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
|
||||
padding: inline_diagnostics.padding.unwrap(),
|
||||
min_column: inline_diagnostics.min_column.unwrap(),
|
||||
max_severity: inline_diagnostics.max_severity.map(Into::into),
|
||||
},
|
||||
},
|
||||
git: git_settings,
|
||||
node: content.node.clone().unwrap().into(),
|
||||
load_direnv: project.load_direnv.clone().unwrap(),
|
||||
session: SessionSettings {
|
||||
restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
|
||||
let project = &content.project;
|
||||
self.context_servers.extend(
|
||||
project
|
||||
.context_servers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, value.into())),
|
||||
);
|
||||
self.dap.extend(
|
||||
project
|
||||
.dap
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value))),
|
||||
);
|
||||
if let Some(diagnostics) = content.diagnostics.as_ref() {
|
||||
if let Some(inline) = &diagnostics.inline {
|
||||
self.diagnostics.inline.enabled.merge_from(&inline.enabled);
|
||||
self.diagnostics
|
||||
.inline
|
||||
.update_debounce_ms
|
||||
.merge_from(&inline.update_debounce_ms);
|
||||
self.diagnostics.inline.padding.merge_from(&inline.padding);
|
||||
self.diagnostics
|
||||
.inline
|
||||
.min_column
|
||||
.merge_from(&inline.min_column);
|
||||
if let Some(max_severity) = inline.max_severity {
|
||||
self.diagnostics.inline.max_severity = Some(max_severity.into())
|
||||
}
|
||||
}
|
||||
|
||||
self.diagnostics.button.merge_from(&diagnostics.button);
|
||||
self.diagnostics
|
||||
.include_warnings
|
||||
.merge_from(&diagnostics.include_warnings);
|
||||
if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
|
||||
self.diagnostics
|
||||
.lsp_pull_diagnostics
|
||||
.enabled
|
||||
.merge_from(&pull_diagnostics.enabled);
|
||||
self.diagnostics
|
||||
.lsp_pull_diagnostics
|
||||
.debounce_ms
|
||||
.merge_from(&pull_diagnostics.debounce_ms);
|
||||
}
|
||||
}
|
||||
if let Some(git) = content.git.as_ref() {
|
||||
if let Some(branch_picker) = git.branch_picker.as_ref() {
|
||||
self.git
|
||||
.branch_picker
|
||||
.show_author_name
|
||||
.merge_from(&branch_picker.show_author_name);
|
||||
}
|
||||
if let Some(inline_blame) = git.inline_blame.as_ref() {
|
||||
self.git
|
||||
.inline_blame
|
||||
.enabled
|
||||
.merge_from(&inline_blame.enabled);
|
||||
self.git
|
||||
.inline_blame
|
||||
.delay_ms
|
||||
.merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
|
||||
self.git
|
||||
.inline_blame
|
||||
.padding
|
||||
.merge_from(&inline_blame.padding);
|
||||
self.git
|
||||
.inline_blame
|
||||
.min_column
|
||||
.merge_from(&inline_blame.min_column);
|
||||
self.git
|
||||
.inline_blame
|
||||
.show_commit_summary
|
||||
.merge_from(&inline_blame.show_commit_summary);
|
||||
}
|
||||
self.git.git_gutter.merge_from(&git.git_gutter);
|
||||
self.git.hunk_style.merge_from(&git.hunk_style);
|
||||
if let Some(debounce) = git.gutter_debounce {
|
||||
self.git.gutter_debounce = Some(debounce);
|
||||
}
|
||||
}
|
||||
self.global_lsp_settings.button.merge_from(
|
||||
&content
|
||||
.global_lsp_settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.button),
|
||||
);
|
||||
self.load_direnv
|
||||
.merge_from(&content.project.load_direnv.clone());
|
||||
|
||||
for (key, value) in content.project.lsp.clone() {
|
||||
self.lsp.insert(LanguageServerName(key.into()), value);
|
||||
}
|
||||
|
||||
if let Some(node) = content.node.as_ref() {
|
||||
self.node
|
||||
.ignore_system_version
|
||||
.merge_from(&node.ignore_system_version);
|
||||
if let Some(path) = node.path.clone() {
|
||||
self.node.path = Some(path);
|
||||
}
|
||||
if let Some(npm_path) = node.npm_path.clone() {
|
||||
self.node.npm_path = Some(npm_path);
|
||||
}
|
||||
}
|
||||
self.session
|
||||
.restore_unsaved_buffers
|
||||
.merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
|
||||
}
|
||||
|
||||
fn import_from_vscode(
|
||||
vscode: &settings::VsCodeSettings,
|
||||
current: &mut settings::SettingsContent,
|
||||
) {
|
||||
// this just sets the binary name instead of a full path so it relies on path lookup
|
||||
// resolving to the one you want
|
||||
vscode.enum_setting(
|
||||
"npm.packageManager",
|
||||
&mut current.node.npm_path,
|
||||
|s| match s {
|
||||
v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
|
||||
_ => None,
|
||||
},
|
||||
);
|
||||
let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
|
||||
v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
|
||||
_ => None,
|
||||
});
|
||||
if npm_path.is_some() {
|
||||
current.node.get_or_insert_default().npm_path = npm_path;
|
||||
}
|
||||
|
||||
if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
|
||||
if let Some(blame) = current.git.inline_blame.as_mut() {
|
||||
blame.enabled = b
|
||||
} else {
|
||||
current.git.inline_blame = Some(InlineBlameSettings {
|
||||
enabled: b,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
current
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = Some(b);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -627,29 +665,27 @@ impl Settings for ProjectSettings {
|
||||
env: Option<HashMap<String, String>>,
|
||||
// note: we don't support envFile and type
|
||||
}
|
||||
impl From<VsCodeContextServerCommand> for ContextServerCommand {
|
||||
fn from(cmd: VsCodeContextServerCommand) -> Self {
|
||||
Self {
|
||||
path: cmd.command,
|
||||
args: cmd.args.unwrap_or_default(),
|
||||
env: cmd.env,
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
|
||||
current
|
||||
.project
|
||||
.context_servers
|
||||
.extend(mcp.iter().filter_map(|(k, v)| {
|
||||
Some((
|
||||
k.clone().into(),
|
||||
ContextServerSettings::Custom {
|
||||
settings::ContextServerSettingsContent::Custom {
|
||||
enabled: true,
|
||||
command: serde_json::from_value::<VsCodeContextServerCommand>(
|
||||
v.clone(),
|
||||
)
|
||||
.ok()?
|
||||
.into(),
|
||||
.ok()
|
||||
.map(|cmd| {
|
||||
settings::ContextServerCommand {
|
||||
path: cmd.command,
|
||||
args: cmd.args.unwrap_or_default(),
|
||||
env: cmd.env,
|
||||
timeout: None,
|
||||
}
|
||||
})?,
|
||||
},
|
||||
))
|
||||
}));
|
||||
@@ -736,17 +772,19 @@ impl SettingsObserver {
|
||||
if let Some(upstream_client) = upstream_client {
|
||||
let mut user_settings = None;
|
||||
user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
|
||||
let new_settings = cx.global::<SettingsStore>().raw_user_settings();
|
||||
if Some(new_settings) != user_settings.as_ref() {
|
||||
if let Some(new_settings_string) = serde_json::to_string(new_settings).ok()
|
||||
{
|
||||
user_settings = Some(new_settings.clone());
|
||||
upstream_client
|
||||
.send(proto::UpdateUserSettings {
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
contents: new_settings_string,
|
||||
})
|
||||
.log_err();
|
||||
if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
|
||||
if Some(new_settings) != user_settings.as_ref() {
|
||||
if let Some(new_settings_string) =
|
||||
serde_json::to_string(new_settings).ok()
|
||||
{
|
||||
user_settings = Some(new_settings.clone());
|
||||
upstream_client
|
||||
.send(proto::UpdateUserSettings {
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
contents: new_settings_string,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -786,6 +824,7 @@ impl SettingsObserver {
|
||||
for worktree in self.worktree_store.read(cx).worktrees() {
|
||||
let worktree_id = worktree.read(cx).id().to_proto();
|
||||
for (path, content) in store.local_settings(worktree.read(cx).id()) {
|
||||
let content = serde_json::to_string(&content).unwrap();
|
||||
downstream_client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id,
|
||||
@@ -856,10 +895,9 @@ impl SettingsObserver {
|
||||
envelope: TypedEnvelope<proto::UpdateUserSettings>,
|
||||
cx: AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
let new_settings = serde_json::from_str::<serde_json::Value>(&envelope.payload.contents)
|
||||
.with_context(|| {
|
||||
format!("deserializing {} user settings", envelope.payload.contents)
|
||||
})?;
|
||||
let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
|
||||
format!("deserializing {} user settings", envelope.payload.contents)
|
||||
})?;
|
||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||
settings_store
|
||||
.set_raw_user_settings(new_settings, cx)
|
||||
@@ -1292,3 +1330,26 @@ pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSett
|
||||
LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DapSettings {
|
||||
pub binary: DapBinary,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<DapSettingsContent> for DapSettings {
|
||||
fn from(content: DapSettingsContent) -> Self {
|
||||
DapSettings {
|
||||
binary: content
|
||||
.binary
|
||||
.map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
|
||||
args: content.args.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DapBinary {
|
||||
Default,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use language::{
|
||||
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
|
||||
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
|
||||
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister,
|
||||
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
|
||||
language_settings::{LanguageSettingsContent, language_settings},
|
||||
tree_sitter_rust, tree_sitter_typescript,
|
||||
};
|
||||
use lsp::{
|
||||
@@ -2246,8 +2246,8 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
||||
// Disable Rust language server, ensuring only that server gets stopped.
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.languages.0.insert(
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.languages_mut().insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
enable_language_server: Some(false),
|
||||
@@ -2265,16 +2265,16 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
|
||||
// former gets started again and that the latter stops.
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |settings, cx| {
|
||||
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.languages.0.insert(
|
||||
LanguageName::new("Rust"),
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.languages_mut().insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
enable_language_server: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
settings.languages.0.insert(
|
||||
LanguageName::new("JavaScript"),
|
||||
settings.languages_mut().insert(
|
||||
"JavaScript".into(),
|
||||
LanguageSettingsContent {
|
||||
enable_language_server: Some(false),
|
||||
..Default::default()
|
||||
@@ -8769,8 +8769,8 @@ async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user