logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
[package]
name = "settings"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/settings.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "fs/test-support"]
[dependencies]
anyhow.workspace = true
collections.workspace = true
ec4rs.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
inventory.workspace = true
log.workspace = true
migrator.workspace = true
paths.workspace = true
release_channel.workspace = true
rust-embed.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings_content.workspace = true
settings_json.workspace = true
settings_macros.workspace = true
smallvec.workspace = true
util.workspace = true
zlog.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
pretty_assertions.workspace = true
unindent.workspace = true

1
crates/settings/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,135 @@
use std::fmt::{Display, Formatter};
use crate::{self as settings, settings_content::BaseKeymapContent};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{RegisterSetting, Settings};
/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
///
/// Default: VSCode
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, RegisterSetting,
)]
pub enum BaseKeymap {
#[default]
VSCode,
JetBrains,
SublimeText,
Atom,
TextMate,
Emacs,
Cursor,
None,
}
impl From<BaseKeymapContent> for BaseKeymap {
fn from(value: BaseKeymapContent) -> Self {
match value {
BaseKeymapContent::VSCode => Self::VSCode,
BaseKeymapContent::JetBrains => Self::JetBrains,
BaseKeymapContent::SublimeText => Self::SublimeText,
BaseKeymapContent::Atom => Self::Atom,
BaseKeymapContent::TextMate => Self::TextMate,
BaseKeymapContent::Emacs => Self::Emacs,
BaseKeymapContent::Cursor => Self::Cursor,
BaseKeymapContent::None => Self::None,
}
}
}
impl Into<BaseKeymapContent> for BaseKeymap {
fn into(self) -> BaseKeymapContent {
match self {
BaseKeymap::VSCode => BaseKeymapContent::VSCode,
BaseKeymap::JetBrains => BaseKeymapContent::JetBrains,
BaseKeymap::SublimeText => BaseKeymapContent::SublimeText,
BaseKeymap::Atom => BaseKeymapContent::Atom,
BaseKeymap::TextMate => BaseKeymapContent::TextMate,
BaseKeymap::Emacs => BaseKeymapContent::Emacs,
BaseKeymap::Cursor => BaseKeymapContent::Cursor,
BaseKeymap::None => BaseKeymapContent::None,
}
}
}
impl Display for BaseKeymap {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
BaseKeymap::VSCode => write!(f, "VS Code"),
BaseKeymap::JetBrains => write!(f, "JetBrains"),
BaseKeymap::SublimeText => write!(f, "Sublime Text"),
BaseKeymap::Atom => write!(f, "Atom"),
BaseKeymap::TextMate => write!(f, "TextMate"),
BaseKeymap::Emacs => write!(f, "Emacs (beta)"),
BaseKeymap::Cursor => write!(f, "Cursor (beta)"),
BaseKeymap::None => write!(f, "None"),
}
}
}
impl BaseKeymap {
#[cfg(target_os = "macos")]
pub const OPTIONS: [(&'static str, Self); 7] = [
("VS Code (Default)", Self::VSCode),
("Atom", Self::Atom),
("JetBrains", Self::JetBrains),
("Sublime Text", Self::SublimeText),
("Emacs (beta)", Self::Emacs),
("TextMate", Self::TextMate),
("Cursor", Self::Cursor),
];
#[cfg(not(target_os = "macos"))]
pub const OPTIONS: [(&'static str, Self); 6] = [
("VS Code (Default)", Self::VSCode),
("Atom", Self::Atom),
("JetBrains", Self::JetBrains),
("Sublime Text", Self::SublimeText),
("Emacs (beta)", Self::Emacs),
("Cursor", Self::Cursor),
];
pub fn asset_path(&self) -> Option<&'static str> {
#[cfg(target_os = "macos")]
match self {
BaseKeymap::JetBrains => Some("keymaps/macos/jetbrains.json"),
BaseKeymap::SublimeText => Some("keymaps/macos/sublime_text.json"),
BaseKeymap::Atom => Some("keymaps/macos/atom.json"),
BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"),
BaseKeymap::Emacs => Some("keymaps/macos/emacs.json"),
BaseKeymap::Cursor => Some("keymaps/macos/cursor.json"),
BaseKeymap::VSCode => None,
BaseKeymap::None => None,
}
#[cfg(not(target_os = "macos"))]
match self {
BaseKeymap::JetBrains => Some("keymaps/linux/jetbrains.json"),
BaseKeymap::SublimeText => Some("keymaps/linux/sublime_text.json"),
BaseKeymap::Atom => Some("keymaps/linux/atom.json"),
BaseKeymap::Emacs => Some("keymaps/linux/emacs.json"),
BaseKeymap::Cursor => Some("keymaps/linux/cursor.json"),
BaseKeymap::TextMate => None,
BaseKeymap::VSCode => None,
BaseKeymap::None => None,
}
}
pub fn names() -> impl Iterator<Item = &'static str> {
Self::OPTIONS.iter().map(|(name, _)| *name)
}
pub fn from_names(option: &str) -> BaseKeymap {
Self::OPTIONS
.iter()
.copied()
.find_map(|(name, value)| (name == option).then_some(value))
.unwrap_or_default()
}
}
impl Settings for BaseKeymap {
fn from_settings(s: &crate::settings_content::SettingsContent) -> Self {
s.base_keymap.unwrap().into()
}
}

View File

@@ -0,0 +1,104 @@
use gpui::{
FontFeatures, FontStyle, FontWeight, Modifiers, Pixels, SharedString,
WindowBackgroundAppearance, px,
};
use settings_content::{
FontFamilyName, FontFeaturesContent, FontSize, FontStyleContent, FontWeightContent,
ModifiersContent, WindowBackgroundContent,
};
use std::sync::Arc;
/// A trait for converting settings content types into their GPUI equivalents.
pub trait IntoGpui {
type Output;
fn into_gpui(self) -> Self::Output;
}
impl IntoGpui for FontStyleContent {
type Output = FontStyle;
fn into_gpui(self) -> Self::Output {
match self {
FontStyleContent::Normal => FontStyle::Normal,
FontStyleContent::Italic => FontStyle::Italic,
FontStyleContent::Oblique => FontStyle::Oblique,
}
}
}
impl IntoGpui for FontWeightContent {
type Output = FontWeight;
fn into_gpui(self) -> Self::Output {
FontWeight(self.0.clamp(100., 950.))
}
}
impl IntoGpui for FontFeaturesContent {
type Output = FontFeatures;
fn into_gpui(self) -> Self::Output {
FontFeatures(Arc::new(self.0.into_iter().collect()))
}
}
impl IntoGpui for WindowBackgroundContent {
type Output = WindowBackgroundAppearance;
fn into_gpui(self) -> Self::Output {
match self {
WindowBackgroundContent::Opaque => WindowBackgroundAppearance::Opaque,
WindowBackgroundContent::Transparent => WindowBackgroundAppearance::Transparent,
WindowBackgroundContent::Blurred => WindowBackgroundAppearance::Blurred,
}
}
}
impl IntoGpui for ModifiersContent {
type Output = Modifiers;
fn into_gpui(self) -> Self::Output {
Modifiers {
control: self.control,
alt: self.alt,
shift: self.shift,
platform: self.platform,
function: self.function,
}
}
}
impl IntoGpui for FontSize {
type Output = Pixels;
fn into_gpui(self) -> Self::Output {
px(self.0)
}
}
impl IntoGpui for FontFamilyName {
type Output = SharedString;
fn into_gpui(self) -> Self::Output {
SharedString::from(self.0)
}
}
#[cfg(test)]
mod tests {
use gpui::FontWeight;
use settings_content::FontWeightContent;
#[test]
fn test_font_weight_content_constants_match_gpui() {
assert_eq!(FontWeightContent::THIN.0, FontWeight::THIN.0);
assert_eq!(FontWeightContent::EXTRA_LIGHT.0, FontWeight::EXTRA_LIGHT.0);
assert_eq!(FontWeightContent::LIGHT.0, FontWeight::LIGHT.0);
assert_eq!(FontWeightContent::NORMAL.0, FontWeight::NORMAL.0);
assert_eq!(FontWeightContent::MEDIUM.0, FontWeight::MEDIUM.0);
assert_eq!(FontWeightContent::SEMIBOLD.0, FontWeight::SEMIBOLD.0);
assert_eq!(FontWeightContent::BOLD.0, FontWeight::BOLD.0);
assert_eq!(FontWeightContent::EXTRA_BOLD.0, FontWeight::EXTRA_BOLD.0);
assert_eq!(FontWeightContent::BLACK.0, FontWeight::BLACK.0);
}
}

View File

@@ -0,0 +1,30 @@
use fs::Fs;
use gpui::{App, RenderOnce, SharedString};
use crate::{settings_content::SettingsContent, update_settings_file};
/// A UI control that can be used to edit a setting.
pub trait EditableSettingControl: RenderOnce {
/// The type of the setting value.
type Value: Send;
/// Returns the name of this setting.
fn name(&self) -> SharedString;
/// Reads the setting value from the settings.
fn read(cx: &App) -> Self::Value;
/// Applies the given setting file to the settings file contents.
///
/// This will be called when writing the setting value back to the settings file.
fn apply(settings: &mut SettingsContent, value: Self::Value, cx: &App);
/// Writes the given setting value to the settings files.
fn write(value: Self::Value, cx: &App) {
let fs = <dyn Fs>::global(cx);
update_settings_file(fs, cx, move |settings, cx| {
Self::apply(settings, value, cx);
});
}
}

View File

@@ -0,0 +1,393 @@
use anyhow::{Context as _, Result};
use collections::{BTreeMap, BTreeSet, HashSet};
use ec4rs::{ConfigParser, PropertiesSource, Section};
use fs::Fs;
use futures::StreamExt;
use gpui::{Context, EventEmitter, Task};
use paths::EDITORCONFIG_NAME;
use smallvec::SmallVec;
use std::{path::Path, str::FromStr, sync::Arc};
use util::{ResultExt as _, rel_path::RelPath};
use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file};
pub type EditorconfigProperties = ec4rs::Properties;
#[derive(Clone)]
pub struct Editorconfig {
pub is_root: bool,
pub sections: SmallVec<[Section; 5]>,
}
impl FromStr for Editorconfig {
type Err = anyhow::Error;
fn from_str(contents: &str) -> Result<Self, Self::Err> {
let parser = ConfigParser::new_buffered(contents.as_bytes())
.context("creating editorconfig parser")?;
let is_root = parser.is_root;
let sections = parser
.collect::<Result<SmallVec<_>, _>>()
.context("parsing editorconfig sections")?;
Ok(Self { is_root, sections })
}
}
#[derive(Clone, Debug)]
pub enum EditorconfigEvent {
ExternalConfigChanged {
path: LocalSettingsPath,
content: Option<String>,
affected_worktree_ids: Vec<WorktreeId>,
},
}
impl EventEmitter<EditorconfigEvent> for EditorconfigStore {}
#[derive(Default)]
pub struct EditorconfigStore {
external_configs: BTreeMap<Arc<Path>, (String, Option<Editorconfig>)>,
worktree_state: BTreeMap<WorktreeId, EditorconfigWorktreeState>,
local_external_config_watchers: BTreeMap<Arc<Path>, Task<()>>,
local_external_config_discovery_tasks: BTreeMap<WorktreeId, Task<()>>,
}
#[derive(Default)]
struct EditorconfigWorktreeState {
internal_configs: BTreeMap<Arc<RelPath>, (String, Option<Editorconfig>)>,
external_config_paths: BTreeSet<Arc<Path>>,
}
impl EditorconfigStore {
pub(crate) fn set_configs(
&mut self,
worktree_id: WorktreeId,
path: LocalSettingsPath,
content: Option<&str>,
) -> std::result::Result<(), InvalidSettingsError> {
match (&path, content) {
(LocalSettingsPath::InWorktree(rel_path), None) => {
if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
state.internal_configs.remove(rel_path);
}
}
(LocalSettingsPath::OutsideWorktree(abs_path), None) => {
if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
state.external_config_paths.remove(abs_path);
}
let still_in_use = self
.worktree_state
.values()
.any(|state| state.external_config_paths.contains(abs_path));
if !still_in_use {
self.external_configs.remove(abs_path);
self.local_external_config_watchers.remove(abs_path);
}
}
(LocalSettingsPath::InWorktree(rel_path), Some(content)) => {
let state = self.worktree_state.entry(worktree_id).or_default();
let should_update = state
.internal_configs
.get(rel_path)
.map_or(true, |entry| entry.0 != content);
if should_update {
let parsed = match content.parse::<Editorconfig>() {
Ok(parsed) => Some(parsed),
Err(e) => {
state
.internal_configs
.insert(rel_path.clone(), (content.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
path: LocalSettingsPath::InWorktree(
rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
),
});
}
};
state
.internal_configs
.insert(rel_path.clone(), (content.to_owned(), parsed));
}
}
(LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => {
let state = self.worktree_state.entry(worktree_id).or_default();
state.external_config_paths.insert(abs_path.clone());
let should_update = self
.external_configs
.get(abs_path)
.map_or(true, |entry| entry.0 != content);
if should_update {
let parsed = match content.parse::<Editorconfig>() {
Ok(parsed) => Some(parsed),
Err(e) => {
self.external_configs
.insert(abs_path.clone(), (content.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
path: LocalSettingsPath::OutsideWorktree(
abs_path.join(EDITORCONFIG_NAME).into(),
),
});
}
};
self.external_configs
.insert(abs_path.clone(), (content.to_owned(), parsed));
}
}
}
Ok(())
}
pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) {
self.local_external_config_discovery_tasks.remove(&root_id);
let Some(removed) = self.worktree_state.remove(&root_id) else {
return;
};
let paths_in_use: HashSet<_> = self
.worktree_state
.values()
.flat_map(|w| w.external_config_paths.iter())
.collect();
for path in removed.external_config_paths.iter() {
if !paths_in_use.contains(path) {
self.external_configs.remove(path);
self.local_external_config_watchers.remove(path);
}
}
}
fn internal_configs(
&self,
root_id: WorktreeId,
) -> impl '_ + Iterator<Item = (&RelPath, &str, Option<&Editorconfig>)> {
self.worktree_state
.get(&root_id)
.into_iter()
.flat_map(|state| {
state
.internal_configs
.iter()
.map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref()))
})
}
fn external_configs(
&self,
worktree_id: WorktreeId,
) -> impl '_ + Iterator<Item = (&Path, &str, Option<&Editorconfig>)> {
self.worktree_state
.get(&worktree_id)
.into_iter()
.flat_map(|state| {
state.external_config_paths.iter().filter_map(|path| {
self.external_configs
.get(path)
.map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref()))
})
})
}
pub fn local_editorconfig_settings(
&self,
worktree_id: WorktreeId,
) -> impl '_ + Iterator<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {
let external = self
.external_configs(worktree_id)
.map(|(path, content, parsed)| {
(
LocalSettingsPath::OutsideWorktree(path.into()),
content,
parsed,
)
});
let internal = self
.internal_configs(worktree_id)
.map(|(path, content, parsed)| {
(LocalSettingsPath::InWorktree(path.into()), content, parsed)
});
external.chain(internal)
}
pub fn discover_local_external_configs_chain(
&mut self,
worktree_id: WorktreeId,
worktree_path: Arc<Path>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) {
// We should only have one discovery task per worktree.
if self
.local_external_config_discovery_tasks
.contains_key(&worktree_id)
{
return;
}
let task = cx.spawn({
let fs = fs.clone();
async move |this, cx| {
let discovered_paths = {
let mut paths = Vec::new();
let mut current = worktree_path.parent().map(|p| p.to_path_buf());
while let Some(dir) = current {
let dir_path: Arc<Path> = Arc::from(dir.as_path());
let path = dir.join(EDITORCONFIG_NAME);
if fs.load(&path).await.is_ok() {
paths.push(dir_path);
}
current = dir.parent().map(|p| p.to_path_buf());
}
paths
};
this.update(cx, |this, cx| {
for dir_path in discovered_paths {
// We insert it here so that watchers can send events to appropriate worktrees.
// external_config_paths gets populated again in set_configs.
this.worktree_state
.entry(worktree_id)
.or_default()
.external_config_paths
.insert(dir_path.clone());
match this.local_external_config_watchers.entry(dir_path.clone()) {
std::collections::btree_map::Entry::Occupied(_) => {
if let Some(existing_config) = this.external_configs.get(&dir_path)
{
cx.emit(EditorconfigEvent::ExternalConfigChanged {
path: LocalSettingsPath::OutsideWorktree(dir_path),
content: Some(existing_config.0.clone()),
affected_worktree_ids: vec![worktree_id],
});
} else {
log::error!("Watcher exists for {dir_path:?} but no config found in external_configs");
}
}
std::collections::btree_map::Entry::Vacant(entry) => {
let watcher =
Self::watch_local_external_config(fs.clone(), dir_path, cx);
entry.insert(watcher);
}
}
}
})
.ok();
}
});
self.local_external_config_discovery_tasks
.insert(worktree_id, task);
}
fn watch_local_external_config(
fs: Arc<dyn Fs>,
dir_path: Arc<Path>,
cx: &mut Context<Self>,
) -> Task<()> {
let config_path = dir_path.join(EDITORCONFIG_NAME);
let (mut config_rx, watcher_task) =
watch_config_file(cx.background_executor(), fs, config_path);
cx.spawn(async move |this, cx| {
let _watcher_task = watcher_task;
while let Some(content) = config_rx.next().await {
let content = Some(content).filter(|c| !c.is_empty());
let dir_path = dir_path.clone();
this.update(cx, |this, cx| {
let affected_worktree_ids: Vec<WorktreeId> = this
.worktree_state
.iter()
.filter_map(|(id, state)| {
state
.external_config_paths
.contains(&dir_path)
.then_some(*id)
})
.collect();
cx.emit(EditorconfigEvent::ExternalConfigChanged {
path: LocalSettingsPath::OutsideWorktree(dir_path),
content,
affected_worktree_ids,
});
})
.ok();
}
})
}
pub fn properties(
&self,
for_worktree: WorktreeId,
for_path: &RelPath,
) -> Option<EditorconfigProperties> {
let mut properties = EditorconfigProperties::new();
let state = self.worktree_state.get(&for_worktree);
let internal_root_config_is_root = state
.and_then(|state| state.internal_configs.get(RelPath::empty()))
.and_then(|data| data.1.as_ref())
.is_some_and(|ec| ec.is_root);
let std_path = for_path.as_std_path();
if !internal_root_config_is_root {
for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
if let Some(parsed_editorconfig) = parsed_editorconfig {
if parsed_editorconfig.is_root {
properties = EditorconfigProperties::new();
}
for section in &parsed_editorconfig.sections {
section.apply_to(&mut properties, std_path).log_err()?;
}
}
}
}
if let Some(state) = state {
let mut internal_configs: SmallVec<[&Editorconfig; 8]> = SmallVec::new();
for ancestor in for_path.ancestors() {
if let Some((_, parsed)) = state.internal_configs.get(ancestor) {
let config = parsed.as_ref()?;
internal_configs.push(config);
if config.is_root {
break;
}
}
}
for config in internal_configs.into_iter().rev() {
if config.is_root {
properties = EditorconfigProperties::new();
}
for section in &config.sections {
section.apply_to(&mut properties, std_path).log_err()?;
}
}
}
properties.use_fallbacks();
Some(properties)
}
}
#[cfg(any(test, feature = "test-support"))]
impl EditorconfigStore {
pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
let watcher_paths: Vec<_> = self
.local_external_config_watchers
.keys()
.cloned()
.collect();
(worktree_ids, external_paths, watcher_paths)
}
pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
self.worktree_state
.get(&worktree_id)
.map(|state| state.external_config_paths.iter().cloned().collect())
.unwrap_or_default()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
mod base_keymap_setting;
mod content_into_gpui;
mod editable_setting_control;
mod editorconfig_store;
mod keymap_file;
mod settings_file;
mod settings_store;
mod vscode_import;
pub use settings_macros::RegisterSetting;
pub mod settings_content {
pub use ::settings_content::*;
}
pub mod fallible_options {
pub use ::settings_content::{FallibleOption, parse_json};
}
#[doc(hidden)]
pub mod private {
pub use crate::settings_store::{RegisteredSetting, SettingValue};
pub use inventory;
}
use gpui::{App, Global};
use rust_embed::RustEmbed;
use std::env;
use std::{borrow::Cow, fmt, str};
use util::asset_str;
pub use ::settings_content::*;
pub use base_keymap_setting::*;
pub use content_into_gpui::IntoGpui;
pub use editable_setting_control::*;
pub use editorconfig_store::{
Editorconfig, EditorconfigEvent, EditorconfigProperties, EditorconfigStore,
};
pub use keymap_file::{
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
};
pub use settings_file::*;
pub use settings_json::*;
pub use settings_store::{
DefaultSemanticTokenRules, InvalidSettingsError, LSP_SETTINGS_SCHEMA_URL_PREFIX,
LocalSettingsKind, LocalSettingsPath, MigrationStatus, Settings, SettingsFile,
SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore,
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
pub use keymap_file::ActionSequence;
#[derive(Clone, Debug, PartialEq)]
pub struct ActiveSettingsProfileName(pub String);
impl Global for ActiveSettingsProfileName {}
pub trait UserSettingsContentExt {
fn for_profile(&self, cx: &App) -> Option<&SettingsProfile>;
fn for_release_channel(&self) -> Option<&SettingsContent>;
fn for_os(&self) -> Option<&SettingsContent>;
}
impl UserSettingsContentExt for UserSettingsContent {
fn for_profile(&self, cx: &App) -> Option<&SettingsProfile> {
let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() else {
return None;
};
self.profiles.get(&active_profile.0)
}
fn for_release_channel(&self) -> Option<&SettingsContent> {
self.release_channel_overrides
.get_by_key(release_channel::RELEASE_CHANNEL.dev_name())
}
fn for_os(&self) -> Option<&SettingsContent> {
self.platform_overrides.get_by_key(env::consts::OS)
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)]
pub struct WorktreeId(usize);
impl From<WorktreeId> for usize {
fn from(value: WorktreeId) -> Self {
value.0
}
}
impl WorktreeId {
pub fn from_usize(handle_id: usize) -> Self {
Self(handle_id)
}
pub fn from_proto(id: u64) -> Self {
Self(id as usize)
}
pub fn to_proto(self) -> u64 {
self.0 as u64
}
pub fn to_usize(self) -> usize {
self.0
}
}
impl fmt::Display for WorktreeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "settings/*"]
#[include = "keymaps/*"]
#[exclude = "*.DS_Store"]
pub struct SettingsAssets;
pub fn init(cx: &mut App) {
let settings = SettingsStore::new(cx, &default_settings());
cx.set_global(settings);
SettingsStore::observe_active_settings_profile_name(cx).detach();
}
pub fn default_settings() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/default.json")
}
pub fn default_semantic_token_rules() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/default_semantic_token_rules.json")
}
#[cfg(target_os = "macos")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
#[cfg(target_os = "windows")]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
pub fn default_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>(DEFAULT_KEYMAP_PATH)
}
pub const VIM_KEYMAP_PATH: &str = "keymaps/vim.json";
pub fn vim_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>(VIM_KEYMAP_PATH)
}
pub fn initial_user_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_user_settings.json")
}
pub fn initial_server_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_server_settings.json")
}
pub fn initial_project_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_settings.json")
}
pub fn initial_keymap_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("keymaps/initial.json")
}
pub fn initial_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_tasks.json")
}
pub fn initial_debug_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_debug_tasks.json")
}
pub fn initial_local_debug_tasks_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_debug_tasks.json")
}

View File

@@ -0,0 +1,277 @@
use crate::{settings_content::SettingsContent, settings_store::SettingsStore};
use collections::HashSet;
use fs::{Fs, PathEventKind};
use futures::{StreamExt, channel::mpsc};
use gpui::{App, BackgroundExecutor, ReadGlobal};
use std::{path::PathBuf, sync::Arc, time::Duration};
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
use serde_json::json;
use std::path::Path;
#[gpui::test]
async fn test_watch_config_dir_reloads_tracked_file_on_rescan(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
let config_dir = PathBuf::from("/root/config");
let settings_path = PathBuf::from("/root/config/settings.json");
fs.insert_tree(
Path::new("/root"),
json!({
"config": {
"settings.json": "A"
}
}),
)
.await;
let mut rx = watch_config_dir(
&cx.background_executor,
fs.clone(),
config_dir.clone(),
HashSet::from_iter([settings_path.clone()]),
);
assert_eq!(rx.next().await.as_deref(), Some("A"));
cx.run_until_parked();
fs.pause_events();
fs.insert_file(&settings_path, b"B".to_vec()).await;
fs.clear_buffered_events();
fs.emit_fs_event(&settings_path, Some(PathEventKind::Rescan));
fs.unpause_events_and_flush();
assert_eq!(rx.next().await.as_deref(), Some("B"));
fs.pause_events();
fs.insert_file(&settings_path, b"A".to_vec()).await;
fs.clear_buffered_events();
fs.emit_fs_event(&config_dir, Some(PathEventKind::Rescan));
fs.unpause_events_and_flush();
assert_eq!(rx.next().await.as_deref(), Some("A"));
}
#[gpui::test]
async fn test_watch_config_file_reloads_when_parent_dir_is_symlink(cx: &mut TestAppContext) {
cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
let config_settings_path = PathBuf::from("/root/.config/zed/settings.json");
let target_settings_path = PathBuf::from("/root/dotfiles/zed/settings.json");
fs.insert_tree(
Path::new("/root"),
json!({
".config": {},
"dotfiles": {
"zed": {
"settings.json": "A"
}
}
}),
)
.await;
fs.create_symlink(
Path::new("/root/.config/zed"),
PathBuf::from("/root/dotfiles/zed"),
)
.await
.unwrap();
let (mut rx, _task) =
watch_config_file(&cx.background_executor, fs.clone(), config_settings_path);
assert_eq!(rx.next().await.as_deref(), Some("A"));
fs.insert_file(&target_settings_path, b"B".to_vec()).await;
assert_eq!(rx.next().await.as_deref(), Some("B"));
}
}
pub const EMPTY_THEME_NAME: &str = "empty-theme";
/// Settings for visual tests that use proper fonts instead of Courier.
/// Uses Helvetica Neue for UI (sans-serif) and Menlo for code (monospace),
/// which are available on all macOS systems.
#[cfg(any(test, feature = "test-support"))]
pub fn visual_test_settings() -> String {
let mut value =
crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
.unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": ".SystemUIFont",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Menlo",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
value.as_object_mut().unwrap().remove("languages");
serde_json::to_string(&value).unwrap()
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
let mut value =
crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
.unwrap();
#[cfg(not(target_os = "windows"))]
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": "Courier",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Courier",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
#[cfg(target_os = "windows")]
util::merge_non_null_json_value_into(
serde_json::json!({
"ui_font_family": "Courier New",
"ui_font_features": {},
"ui_font_size": 14,
"ui_font_fallback": [],
"buffer_font_family": "Courier New",
"buffer_font_features": {},
"buffer_font_size": 14,
"buffer_font_fallbacks": [],
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
value.as_object_mut().unwrap().remove("languages");
serde_json::to_string(&value).unwrap()
}
pub fn watch_config_file(
executor: &BackgroundExecutor,
fs: Arc<dyn Fs>,
path: PathBuf,
) -> (mpsc::UnboundedReceiver<String>, gpui::Task<()>) {
let (tx, rx) = mpsc::unbounded();
let task = executor.spawn(async move {
let path = fs.canonicalize(&path).await.unwrap_or_else(|_| path);
let (events, _) = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await
&& tx.unbounded_send(contents).is_err()
{
break;
}
}
});
(rx, task)
}
pub fn watch_config_dir(
executor: &BackgroundExecutor,
fs: Arc<dyn Fs>,
dir_path: PathBuf,
config_paths: HashSet<PathBuf>,
) -> mpsc::UnboundedReceiver<String> {
let (tx, rx) = mpsc::unbounded();
executor
.spawn(async move {
for file_path in &config_paths {
if fs.metadata(file_path).await.is_ok_and(|v| v.is_some())
&& let Ok(contents) = fs.load(file_path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
while let Some(event_batch) = events.next().await {
for event in event_batch {
if config_paths.contains(&event.path) {
match event.kind {
Some(PathEventKind::Removed) => {
if tx.unbounded_send(String::new()).is_err() {
return;
}
}
Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
if let Ok(contents) = fs.load(&event.path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
Some(PathEventKind::Rescan) => {
for file_path in &config_paths {
if let Ok(contents) = fs.load(file_path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
}
_ => {}
}
} else if matches!(event.kind, Some(PathEventKind::Rescan))
&& event.path == dir_path
{
for file_path in &config_paths {
if let Ok(contents) = fs.load(file_path).await
&& tx.unbounded_send(contents).is_err()
{
return;
}
}
}
}
}
})
.detach();
rx
}
pub fn update_settings_file(
fs: Arc<dyn Fs>,
cx: &App,
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
) {
SettingsStore::global(cx).update_settings_file(fs, update)
}
pub fn update_settings_file_with_completion(
fs: Arc<dyn Fs>,
cx: &App,
update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
) -> futures::channel::oneshot::Receiver<anyhow::Result<()>> {
SettingsStore::global(cx).update_settings_file_with_completion(fs, update)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff