settings_ui: Add maybe settings (#40724)

Closes #ISSUE

Adds a `Maybe<T>` type to `settings_content`, that makes the distinction
between `null` and omitted settings values explicit. This unlocks a few
more settings in the settings UI

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle
2025-10-20 16:25:20 -04:00
committed by GitHub
parent 33bc586ed1
commit ebaefa8cbc
8 changed files with 434 additions and 31 deletions
+193 -22
View File
@@ -9,6 +9,16 @@ use crate::{
SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack,
};
const DEFAULT_STRING: String = String::new();
/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe`
/// to avoid the "NO DEFAULT" case.
const DEFAULT_EMPTY_STRING: Option<&String> = Some(&DEFAULT_STRING);
const DEFAULT_SHARED_STRING: SharedString = SharedString::new_static("");
/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe`
/// to avoid the "NO DEFAULT" case.
const DEFAULT_EMPTY_SHARED_STRING: Option<&SharedString> = Some(&DEFAULT_SHARED_STRING);
pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
vec![
SettingsPage {
@@ -16,16 +26,20 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
items: vec![
SettingsPageItem::SectionHeader("General Settings"),
SettingsPageItem::SettingItem(SettingItem {
title: "Confirm Quit",
description: "Confirm before quitting Zed",
field: Box::new(SettingField {
pick: |settings_content| settings_content.workspace.confirm_quit.as_ref(),
write: |settings_content, value| {
settings_content.workspace.confirm_quit = value;
},
}),
metadata: None,
files: USER,
files: LOCAL,
title: "Project Name",
description: "The Displayed Name Of This Project. If Left Empty, The Root Directory Name Will Be Displayed",
field: Box::new(
SettingField {
pick: |settings_content| {
settings_content.project.worktree.project_name.as_ref()?.as_ref().or(DEFAULT_EMPTY_STRING)
},
write: |settings_content, value| {
settings_content.project.worktree.project_name = settings::Maybe::Set(value.filter(|name| !name.is_empty()));
},
}
),
metadata: Some(Box::new(SettingsFieldMetadata { placeholder: Some("Project Name"), ..Default::default() })),
}),
SettingsPageItem::SettingItem(SettingItem {
title: "When Closing With No Tabs",
@@ -4205,26 +4219,183 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
title: "Terminal",
items: vec![
SettingsPageItem::SectionHeader("Environment"),
SettingsPageItem::SettingItem(SettingItem {
title: "Shell",
description: "What shell to use when opening a terminal",
field: Box::new(
SettingField {
SettingsPageItem::DynamicItem(DynamicItem {
discriminant: SettingItem {
files: USER | LOCAL,
title: "Shell",
description: "What shell to use when opening a terminal",
field: Box::new(SettingField {
pick: |settings_content| {
settings_content.terminal.as_ref()?.project.shell.as_ref()
Some(&dynamic_variants::<settings::Shell>()[
settings_content
.terminal
.as_ref()?
.project
.shell
.as_ref()?
.discriminant() as usize])
},
write: |settings_content, value| {
settings_content
let Some(value) = value else {
return;
};
let settings_value = settings_content
.terminal
.get_or_insert_default()
.project
.shell = value;
.shell
.get_or_insert_with(|| settings::Shell::default());
*settings_value = match value {
settings::ShellDiscriminants::System => {
settings::Shell::System
},
settings::ShellDiscriminants::Program => {
let program = match settings_value {
settings::Shell::Program(p) => p.clone(),
settings::Shell::WithArguments { program, .. } => program.clone(),
_ => String::from("sh"),
};
settings::Shell::Program(program)
},
settings::ShellDiscriminants::WithArguments => {
let (program, args, title_override) = match settings_value {
settings::Shell::Program(p) => (p.clone(), vec![], None),
settings::Shell::WithArguments { program, args, title_override } => {
(program.clone(), args.clone(), title_override.clone())
},
_ => (String::from("sh"), vec![], None),
};
settings::Shell::WithArguments {
program,
args,
title_override,
}
},
};
},
}),
metadata: None,
},
pick_discriminant: |settings_content| {
Some(settings_content.terminal.as_ref()?.project.shell.as_ref()?.discriminant() as usize)
},
fields: dynamic_variants::<settings::Shell>().into_iter().map(|variant| {
match variant {
settings::ShellDiscriminants::System => vec![],
settings::ShellDiscriminants::Program => vec![
SettingItem {
files: USER | LOCAL,
title: "Program",
description: "The shell program to use",
field: Box::new(SettingField {
pick: |settings_content| {
match settings_content.terminal.as_ref()?.project.shell.as_ref() {
Some(settings::Shell::Program(program)) => Some(program),
_ => None
}
},
write: |settings_content, value| {
let Some(value) = value else {
return;
};
match settings_content
.terminal
.get_or_insert_default()
.project
.shell.as_mut() {
Some(settings::Shell::Program(program)) => *program = value,
_ => return
}
},
}),
metadata: None,
}
],
settings::ShellDiscriminants::WithArguments => vec![
SettingItem {
files: USER | LOCAL,
title: "Program",
description: "The shell program to run",
field: Box::new(SettingField {
pick: |settings_content| {
match settings_content.terminal.as_ref()?.project.shell.as_ref() {
Some(settings::Shell::WithArguments { program, .. }) => Some(program),
_ => None
}
},
write: |settings_content, value| {
let Some(value) = value else {
return;
};
match settings_content
.terminal
.get_or_insert_default()
.project
.shell.as_mut() {
Some(settings::Shell::WithArguments { program, .. }) => *program = value,
_ => return
}
},
}),
metadata: None,
},
SettingItem {
files: USER | LOCAL,
title: "Arguments",
description: "The arguments to pass to the shell program",
field: Box::new(
SettingField {
pick: |settings_content| {
match settings_content.terminal.as_ref()?.project.shell.as_ref() {
Some(settings::Shell::WithArguments { args, .. }) => Some(args),
_ => None
}
},
write: |settings_content, value| {
let Some(value) = value else {
return;
};
match settings_content
.terminal
.get_or_insert_default()
.project
.shell.as_mut() {
Some(settings::Shell::WithArguments { args, .. }) => *args = value,
_ => return
}
},
}
.unimplemented(),
),
metadata: None,
},
SettingItem {
files: USER | LOCAL,
title: "Title Override",
description: "An optional string to override the title of the terminal tab",
field: Box::new(SettingField {
pick: |settings_content| {
match settings_content.terminal.as_ref()?.project.shell.as_ref() {
Some(settings::Shell::WithArguments { title_override, .. }) => title_override.as_ref().or(DEFAULT_EMPTY_SHARED_STRING),
_ => None
}
},
write: |settings_content, value| {
match settings_content
.terminal
.get_or_insert_default()
.project
.shell.as_mut() {
Some(settings::Shell::WithArguments { title_override, .. }) => *title_override = value.filter(|s| !s.is_empty()),
_ => return
}
},
}),
metadata: None,
}
],
}
.unimplemented(),
),
metadata: None,
files: USER | LOCAL,
}).collect(),
}),
SettingsPageItem::DynamicItem(DynamicItem {
discriminant: SettingItem {
+3
View File
@@ -370,6 +370,7 @@ fn init_renderers(cx: &mut App) {
})
.add_basic_renderer::<bool>(render_toggle_button)
.add_basic_renderer::<String>(render_text_field)
.add_basic_renderer::<SharedString>(render_text_field)
.add_basic_renderer::<settings::SaturatingBool>(render_toggle_button)
.add_basic_renderer::<settings::CursorShape>(render_dropdown)
.add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
@@ -444,8 +445,10 @@ fn init_renderers(cx: &mut App) {
.add_basic_renderer::<settings::BufferLineHeightDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::AutosaveSettingDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::WorkingDirectoryDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::MaybeDiscriminants>(render_dropdown)
.add_basic_renderer::<settings::IncludeIgnoredContent>(render_dropdown)
.add_basic_renderer::<settings::ShowIndentGuides>(render_dropdown)
.add_basic_renderer::<settings::ShellDiscriminants>(render_dropdown)
// please semicolon stay on next line
;
}