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:
48
crates/settings/Cargo.toml
Normal file
48
crates/settings/Cargo.toml
Normal 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
1
crates/settings/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
135
crates/settings/src/base_keymap_setting.rs
Normal file
135
crates/settings/src/base_keymap_setting.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
104
crates/settings/src/content_into_gpui.rs
Normal file
104
crates/settings/src/content_into_gpui.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
30
crates/settings/src/editable_setting_control.rs
Normal file
30
crates/settings/src/editable_setting_control.rs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
393
crates/settings/src/editorconfig_store.rs
Normal file
393
crates/settings/src/editorconfig_store.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
2585
crates/settings/src/keymap_file.rs
Normal file
2585
crates/settings/src/keymap_file.rs
Normal file
File diff suppressed because it is too large
Load Diff
184
crates/settings/src/settings.rs
Normal file
184
crates/settings/src/settings.rs
Normal 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")
|
||||
}
|
||||
277
crates/settings/src/settings_file.rs
Normal file
277
crates/settings/src/settings_file.rs
Normal 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)
|
||||
}
|
||||
3012
crates/settings/src/settings_store.rs
Normal file
3012
crates/settings/src/settings_store.rs
Normal file
File diff suppressed because it is too large
Load Diff
1113
crates/settings/src/vscode_import.rs
Normal file
1113
crates/settings/src/vscode_import.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user