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:
Conrad Irwin
2025-09-18 16:47:23 +00:00
committed by GitHub
co-authored by Ben Kunkle Mikayla Maki Anthony
parent 0a9023bce0
commit fcdab160f9
219 changed files with 11697 additions and 11894 deletions
+148 -293
View File
@@ -2,22 +2,18 @@ use alacritty_terminal::vte::ansi::{
CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
};
use collections::HashMap;
use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{SettingsKey, SettingsSources, SettingsUi};
use std::path::PathBuf;
pub use settings::AlternateScroll;
use settings::{
CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
};
use task::Shell;
use theme::FontFamilyName;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TerminalDockPosition {
Left,
Bottom,
Right,
}
use util::MergeFrom;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Toolbar {
@@ -28,7 +24,7 @@ pub struct Toolbar {
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
pub font_size: Option<Pixels>,
pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
pub font_family: Option<FontFamilyName>,
pub font_fallbacks: Option<FontFallbacks>,
pub font_features: Option<FontFeatures>,
@@ -60,218 +56,135 @@ pub struct ScrollbarSettings {
pub show: Option<ShowScrollbar>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarSettingsContent {
/// When to show the scrollbar in the terminal.
///
/// Default: inherits editor scrollbar settings
pub show: Option<Option<ShowScrollbar>>,
}
/// When to show the scrollbar in the terminal.
///
/// Default: auto
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
/// Show the scrollbar if there's important information or
/// follow the system's configured behavior.
Auto,
/// Match the system's configured behavior.
System,
/// Always show the scrollbar.
Always,
/// Never show the scrollbar.
Never,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum VenvSettings {
#[default]
Off,
On {
/// Default directories to search for virtual environments, relative
/// to the current working directory. We recommend overriding this
/// in your project's settings, rather than globally.
activate_script: Option<ActivateScript>,
venv_name: Option<String>,
directories: Option<Vec<PathBuf>>,
},
}
pub struct VenvSettingsContent<'a> {
pub activate_script: ActivateScript,
pub venv_name: &'a str,
pub directories: &'a [PathBuf],
}
impl VenvSettings {
pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
match self {
VenvSettings::Off => None,
VenvSettings::On {
activate_script,
venv_name,
directories,
} => Some(VenvSettingsContent {
activate_script: activate_script.unwrap_or(ActivateScript::Default),
venv_name: venv_name.as_deref().unwrap_or(""),
directories: directories.as_deref().unwrap_or(&[]),
}),
}
fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
match shell {
settings::Shell::System => Shell::System,
settings::Shell::Program(program) => Shell::Program(program),
settings::Shell::WithArguments {
program,
args,
title_override,
} => Shell::WithArguments {
program,
args,
title_override,
},
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ActivateScript {
#[default]
Default,
Csh,
Fish,
Nushell,
PowerShell,
Pyenv,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(key = "terminal")]
pub struct TerminalSettingsContent {
/// What shell to use when opening a terminal.
///
/// Default: system
pub shell: Option<Shell>,
/// What working directory to use when launching the terminal
///
/// Default: current_project_directory
pub working_directory: Option<WorkingDirectory>,
/// Sets the terminal's font size.
///
/// If this option is not included,
/// the terminal will default to matching the buffer's font size.
pub font_size: Option<f32>,
/// Sets the terminal's font family.
///
/// If this option is not included,
/// the terminal will default to matching the buffer's font family.
pub font_family: Option<FontFamilyName>,
/// Sets the terminal's font fallbacks.
///
/// If this option is not included,
/// the terminal will default to matching the buffer's font fallbacks.
#[schemars(extend("uniqueItems" = true))]
pub font_fallbacks: Option<Vec<FontFamilyName>>,
/// Sets the terminal's line height.
///
/// Default: comfortable
pub line_height: Option<TerminalLineHeight>,
pub font_features: Option<FontFeatures>,
/// Sets the terminal's font weight in CSS weight units 0-900.
pub font_weight: Option<f32>,
/// Any key-value pairs added to this list will be added to the terminal's
/// environment. Use `:` to separate multiple values.
///
/// Default: {}
pub env: Option<HashMap<String, String>>,
/// Default cursor shape for the terminal.
/// Can be "bar", "block", "underline", or "hollow".
///
/// Default: None
pub cursor_shape: Option<CursorShape>,
/// Sets the cursor blinking behavior in the terminal.
///
/// Default: terminal_controlled
pub blinking: Option<TerminalBlink>,
/// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
/// Alternate Scroll mode converts mouse scroll events into up / down key
/// presses when in the alternate screen (e.g. when running applications
/// like vim or less). The terminal can still set and unset this mode.
///
/// Default: on
pub alternate_scroll: Option<AlternateScroll>,
/// Sets whether the option key behaves as the meta key.
///
/// Default: false
pub option_as_meta: Option<bool>,
/// Whether or not selecting text in the terminal will automatically
/// copy to the system clipboard.
///
/// Default: false
pub copy_on_select: Option<bool>,
/// Whether to keep the text selection after copying it to the clipboard.
///
/// Default: false
pub keep_selection_on_copy: Option<bool>,
/// Whether to show the terminal button in the status bar.
///
/// Default: true
pub button: Option<bool>,
pub dock: Option<TerminalDockPosition>,
/// Default width when the terminal is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default height when the terminal is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>,
/// Activates the python virtual environment, if one is found, in the
/// terminal's working directory (as resolved by the working_directory
/// setting). Set this to "off" to disable this behavior.
///
/// Default: on
pub detect_venv: Option<VenvSettings>,
/// The maximum number of lines to keep in the scrollback history.
/// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
/// 0 disables the scrolling.
/// Existing terminals will not pick up this change until they are recreated.
/// See <a href="https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213">Alacritty documentation</a> for more information.
///
/// Default: 10_000
pub max_scroll_history_lines: Option<usize>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
/// The minimum APCA perceptual contrast between foreground and background colors.
///
/// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
/// especially for dark mode. Values range from 0 to 106.
///
/// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
/// https://readtech.org/ARC/tests/bronze-simple-mode/
/// - 0: No contrast adjustment
/// - 45: Minimum for large fluent text (36px+)
/// - 60: Minimum for other content text
/// - 75: Minimum for body text
/// - 90: Preferred for body text
///
/// Default: 45
pub minimum_contrast: Option<f32>,
}
impl settings::Settings for TerminalSettings {
type FileContent = TerminalSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
let settings: Self = sources.json_merge()?;
// Validate minimum_contrast for APCA
if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
anyhow::bail!(
"terminal.minimum_contrast must be between 0 and 106, but got {}. \
APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
settings.minimum_contrast
);
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
let content = content.terminal.clone().unwrap();
TerminalSettings {
shell: settings_shell_to_task_shell(content.shell.unwrap()),
working_directory: content.working_directory.unwrap(),
font_size: content.font_size.map(px),
font_family: content.font_family,
font_fallbacks: content.font_fallbacks.map(|fallbacks| {
FontFallbacks::from_fonts(
fallbacks
.into_iter()
.map(|family| family.0.to_string())
.collect(),
)
}),
font_features: content.font_features,
font_weight: content.font_weight.map(FontWeight),
line_height: content.line_height.unwrap(),
env: content.env.unwrap(),
cursor_shape: content.cursor_shape.map(Into::into),
blinking: content.blinking.unwrap(),
alternate_scroll: content.alternate_scroll.unwrap(),
option_as_meta: content.option_as_meta.unwrap(),
copy_on_select: content.copy_on_select.unwrap(),
keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
button: content.button.unwrap(),
dock: content.dock.unwrap(),
default_width: px(content.default_width.unwrap()),
default_height: px(content.default_height.unwrap()),
detect_venv: content.detect_venv.unwrap(),
max_scroll_history_lines: content.max_scroll_history_lines,
toolbar: Toolbar {
breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
},
scrollbar: ScrollbarSettings {
show: content.scrollbar.unwrap().show.flatten(),
},
minimum_contrast: content.minimum_contrast.unwrap(),
}
Ok(settings)
}
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.terminal else {
return;
};
self.shell
.merge_from(&content.shell.clone().map(settings_shell_to_task_shell));
self.working_directory
.merge_from(&content.working_directory);
if let Some(font_size) = content.font_size.map(px) {
self.font_size = Some(font_size)
}
if let Some(font_family) = content.font_family.clone() {
self.font_family = Some(font_family);
}
if let Some(fallbacks) = content.font_fallbacks.clone() {
self.font_fallbacks = Some(FontFallbacks::from_fonts(
fallbacks
.into_iter()
.map(|family| family.0.to_string())
.collect(),
))
}
if let Some(font_features) = content.font_features.clone() {
self.font_features = Some(font_features)
}
if let Some(font_weight) = content.font_weight {
self.font_weight = Some(FontWeight(font_weight));
}
self.line_height.merge_from(&content.line_height);
if let Some(env) = &content.env {
for (key, value) in env {
self.env.insert(key.clone(), value.clone());
}
}
if let Some(cursor_shape) = content.cursor_shape {
self.cursor_shape = Some(cursor_shape.into())
}
self.blinking.merge_from(&content.blinking);
self.alternate_scroll.merge_from(&content.alternate_scroll);
self.option_as_meta.merge_from(&content.option_as_meta);
self.copy_on_select.merge_from(&content.copy_on_select);
self.keep_selection_on_copy
.merge_from(&content.keep_selection_on_copy);
self.button.merge_from(&content.button);
self.dock.merge_from(&content.dock);
self.default_width
.merge_from(&content.default_width.map(px));
self.default_height
.merge_from(&content.default_height.map(px));
self.detect_venv.merge_from(&content.detect_venv);
if let Some(max_scroll_history_lines) = content.max_scroll_history_lines {
self.max_scroll_history_lines = Some(max_scroll_history_lines)
}
self.toolbar.breadcrumbs.merge_from(
&content
.toolbar
.as_ref()
.and_then(|toolbar| toolbar.breadcrumbs),
);
self.scrollbar.show.merge_from(
&content
.scrollbar
.as_ref()
.and_then(|scrollbar| scrollbar.show),
);
self.minimum_contrast.merge_from(&content.minimum_contrast);
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
let mut default = TerminalSettingsContent::default();
let current = content.terminal.as_mut().unwrap_or(&mut default);
let name = |s| format!("terminal.integrated.{s}");
vscode.f32_setting(&name("fontSize"), &mut current.font_size);
@@ -290,9 +203,9 @@ impl settings::Settings for TerminalSettings {
&name("cursorStyle"),
&mut current.cursor_shape,
|s| match s {
"block" => Some(CursorShape::Block),
"line" => Some(CursorShape::Bar),
"underline" => Some(CursorShape::Underline),
"block" => Some(CursorShapeContent::Block),
"line" => Some(CursorShapeContent::Bar),
"underline" => Some(CursorShapeContent::Underline),
_ => None,
},
);
@@ -316,7 +229,7 @@ impl settings::Settings for TerminalSettings {
// TODO: handle arguments
let shell_name = format!("{platform}Exec");
if let Some(s) = vscode.read_string(&name(&shell_name)) {
current.shell = Some(Shell::Program(s.to_owned()))
current.shell = Some(settings::Shell::Program(s.to_owned()))
}
if let Some(env) = vscode
@@ -337,81 +250,12 @@ impl settings::Settings for TerminalSettings {
}
}
}
if content.terminal.is_none() && default != TerminalSettingsContent::default() {
content.terminal = Some(default)
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
pub enum TerminalLineHeight {
/// Use a line height that's comfortable for reading, 1.618
#[default]
Comfortable,
/// Use a standard line height, 1.3. This option is useful for TUIs,
/// particularly if they use box characters
Standard,
/// Use a custom line height.
Custom(f32),
}
impl TerminalLineHeight {
pub fn value(&self) -> AbsoluteLength {
let value = match self {
TerminalLineHeight::Comfortable => 1.618,
TerminalLineHeight::Standard => 1.3,
TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
};
px(value).into()
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TerminalBlink {
/// Never blink the cursor, ignoring the terminal mode.
Off,
/// Default the cursor blink to off, but allow the terminal to
/// set blinking.
TerminalControlled,
/// Always blink the cursor, ignoring the terminal mode.
On,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AlternateScroll {
On,
Off,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectory {
/// Use the current file's project directory. Will Fallback to the
/// first project directory strategy if unsuccessful.
CurrentProjectDirectory,
/// Use the first project in this workspace's directory.
FirstProjectDirectory,
/// Always use this platform's home directory (if it can be found).
AlwaysHome,
/// Always use a specific directory. This value will be shell expanded.
/// If this path is not a valid directory the terminal will default to
/// this platform's home directory (if it can be found).
Always { directory: String },
}
// Toolbar related settings
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ToolbarContent {
/// Whether to display the terminal title in breadcrumbs inside the terminal pane.
/// Only shown if the terminal title is not empty.
///
/// The shell running in the terminal needs to be configured to emit the title.
/// Example: `echo -e "\e]2;New Title\007";`
///
/// Default: true
pub breadcrumbs: Option<bool>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CursorShape {
@@ -426,6 +270,17 @@ pub enum CursorShape {
Hollow,
}
impl From<settings::CursorShapeContent> for CursorShape {
fn from(value: settings::CursorShapeContent) -> Self {
match value {
settings::CursorShapeContent::Block => CursorShape::Block,
settings::CursorShapeContent::Underline => CursorShape::Underline,
settings::CursorShapeContent::Bar => CursorShape::Bar,
settings::CursorShapeContent::Hollow => CursorShape::Hollow,
}
}
}
impl From<CursorShape> for AlacCursorShape {
fn from(value: CursorShape) -> Self {
match value {