From 93968f256333a9c89ba99e9215e404c1b8b2c273 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Sep 2025 14:10:33 -0600 Subject: [PATCH] Vim --- Cargo.lock | 22 - Cargo.toml | 1 - crates/settings/src/settings_content.rs | 6 +- crates/settings_ui/Cargo.toml | 42 - crates/settings_ui/LICENSE-GPL | 1 - crates/settings_ui/src/settings_ui.rs | 940 ------------------ crates/vim/src/digraph.rs | 1 - crates/vim/src/normal.rs | 3 +- crates/vim/src/normal/paste.rs | 10 +- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/normal/search.rs | 1 - crates/vim/src/test.rs | 5 +- .../src/test/neovim_backed_test_context.rs | 13 +- crates/vim/src/test/vim_test_context.rs | 10 +- crates/vim/src/vim.rs | 57 +- crates/zed/Cargo.toml | 2 - crates/zed/src/main.rs | 1 - crates/zed/src/zed.rs | 48 +- crates/zeta/src/init.rs | 2 +- crates/zeta_cli/src/headless.rs | 2 +- 20 files changed, 76 insertions(+), 1093 deletions(-) delete mode 100644 crates/settings_ui/Cargo.toml delete mode 120000 crates/settings_ui/LICENSE-GPL delete mode 100644 crates/settings_ui/src/settings_ui.rs diff --git a/Cargo.lock b/Cargo.lock index c98a90bb09f0f8b4e9f43facb6bb67c5df2f8e40..7d6f16a3f3e5149c4dd403f3721040a39b00f85a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14925,27 +14925,6 @@ dependencies = [ "zed_actions", ] -[[package]] -name = "settings_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette_hooks", - "debugger_ui", - "editor", - "feature_flags", - "gpui", - "menu", - "serde", - "serde_json", - "settings", - "smallvec", - "theme", - "ui", - "workspace", - "workspace-hack", -] - [[package]] name = "settings_ui_macros" version = "0.1.0" @@ -20625,7 +20604,6 @@ dependencies = [ "session", "settings", "settings_profile_selector", - "settings_ui", "shellexpand 2.1.2", "smol", "snippet_provider", diff --git a/Cargo.toml b/Cargo.toml index de1d216561b9d3074eecff5c36d6405a29c4f7d3..3c1f5dd97692e191977b77b5a1254eda9f9b4e7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,7 +148,6 @@ members = [ "crates/session", "crates/settings", "crates/settings_profile_selector", - "crates/settings_ui", "crates/settings_ui_macros", "crates/snippet", "crates/snippet_provider", diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 55ec782e83369a571b958bacb2a3c4d223ce8567..5321201384edf12e5ad662be9e54d70499713c99 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -552,16 +552,12 @@ pub struct VimSettingsContent { pub cursor_shape: Option, } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] +#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)] #[serde(rename_all = "snake_case")] pub enum ModeContent { #[default] Normal, Insert, - Replace, - Visual, - VisualLine, - VisualBlock, HelixNormal, } diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml deleted file mode 100644 index 1d8e5e11226103e2ef7778816f5c1b41cd934b05..0000000000000000000000000000000000000000 --- a/crates/settings_ui/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "settings_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_ui.rs" - -[features] -default = [] -test-support = [] - -[dependencies] -anyhow.workspace = true -command_palette_hooks.workspace = true -editor.workspace = true -feature_flags.workspace = true -gpui.workspace = true -menu.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true -workspace-hack.workspace = true - - -[dev-dependencies] -debugger_ui.workspace = true - -# Uncomment other workspace dependencies as needed -# assistant.workspace = true -# client.workspace = true -# project.workspace = true -# settings.workspace = true diff --git a/crates/settings_ui/LICENSE-GPL b/crates/settings_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/settings_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs deleted file mode 100644 index c5c633e73f90cf4f3c4e5243009ec6f34e905e6e..0000000000000000000000000000000000000000 --- a/crates/settings_ui/src/settings_ui.rs +++ /dev/null @@ -1,940 +0,0 @@ -use std::{ - num::NonZeroU32, - ops::{Not, Range}, - rc::Rc, -}; - -use anyhow::Context as _; -use editor::Editor; -use feature_flags::{FeatureFlag, FeatureFlagAppExt}; -use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions}; -use settings::{ - NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem, - SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion, - SettingsValue, -}; -use smallvec::SmallVec; -use ui::{ - ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, - prelude::*, -}; -use workspace::{ - Workspace, - item::{Item, ItemEvent}, -}; - -pub struct SettingsUiFeatureFlag; - -impl FeatureFlag for SettingsUiFeatureFlag { - const NAME: &'static str = "settings-ui"; -} - -actions!( - zed, - [ - /// Opens settings UI. - OpenSettingsUi - ] -); - -pub fn open_settings_editor( - workspace: &mut Workspace, - _: &OpenSettingsUi, - window: &mut Window, - cx: &mut Context, -) { - // todo(settings_ui) open in a local workspace if this is remote. - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = SettingsPage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx) - } -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action_renderer(|div, _, _, cx| { - let settings_ui_actions = [std::any::TypeId::of::()]; - let has_flag = cx.has_flag::(); - command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| { - if has_flag { - filter.show_action_types(&settings_ui_actions); - } else { - filter.hide_action_types(&settings_ui_actions); - } - }); - if has_flag { - div.on_action(cx.listener(open_settings_editor)) - } else { - div - } - }); - }) - .detach(); -} - -pub struct SettingsPage { - focus_handle: FocusHandle, - settings_tree: SettingsUiTree, -} - -impl SettingsPage { - pub fn new(_workspace: &Workspace, cx: &mut Context) -> Entity { - cx.new(|cx| Self { - focus_handle: cx.focus_handle(), - settings_tree: SettingsUiTree::new(cx), - }) - } -} - -impl EventEmitter for SettingsPage {} - -impl Focusable for SettingsPage { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for SettingsPage { - type Event = ItemEvent; - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::Settings)) - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Settings".into() - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } -} - -// We want to iterate over the side bar with root groups -// - this is a loop over top level groups, and if any are expanded, recursively displaying their items -// - Should be able to get all items from a group (flatten a group) -// - Should be able to toggle/untoggle groups in UI (at least in sidebar) -// - Search should be available -// - there should be an index of text -> item mappings, for using fuzzy::match -// - Do we want to show the parent groups when a item is matched? - -struct UiEntry { - title: SharedString, - path: Option, - documentation: Option, - _depth: usize, - // a - // b < a descendant range < a total descendant range - // f | | - // g | | - // c < | - // d | - // e < - descendant_range: Range, - total_descendant_range: Range, - next_sibling: Option, - // expanded: bool, - render: Option, - dynamic_render: Option, - generate_items: Option<( - SettingsUiItem, - fn(&serde_json::Value, &App) -> Vec, - SmallVec<[SharedString; 1]>, - )>, -} - -impl UiEntry { - fn first_descendant_index(&self) -> Option { - return self - .descendant_range - .is_empty() - .not() - .then_some(self.descendant_range.start); - } - - fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option { - let first_descendant_index = self.first_descendant_index()?; - let mut current_index = 0; - let mut current_descendant_index = Some(first_descendant_index); - while let Some(descendant_index) = current_descendant_index - && current_index < n - { - current_index += 1; - current_descendant_index = tree[descendant_index].next_sibling; - } - current_descendant_index - } -} - -pub struct SettingsUiTree { - root_entry_indices: Vec, - entries: Vec, - active_entry_index: usize, -} - -fn build_tree_item( - tree: &mut Vec, - entry: SettingsUiEntry, - depth: usize, - prev_index: Option, -) { - // let tree: HashMap; - let index = tree.len(); - tree.push(UiEntry { - title: entry.title.into(), - path: entry.path.map(SharedString::new_static), - documentation: entry.documentation.map(SharedString::new_static), - _depth: depth, - descendant_range: index + 1..index + 1, - total_descendant_range: index + 1..index + 1, - render: None, - next_sibling: None, - dynamic_render: None, - generate_items: None, - }); - if let Some(prev_index) = prev_index { - tree[prev_index].next_sibling = Some(index); - } - match entry.item { - SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => { - for group_item in group_items { - let prev_index = tree[index] - .descendant_range - .is_empty() - .not() - .then_some(tree[index].descendant_range.end - 1); - tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, group_item, depth + 1, prev_index); - tree[index].total_descendant_range.end = tree.len(); - } - } - SettingsUiItem::Single(item) => { - tree[index].render = Some(item); - } - SettingsUiItem::Union(dynamic_render) => { - // todo(settings_ui) take from item and store other fields instead of clone - // will also require replacing usage in render_recursive so it can know - // which options were actually rendered - let options = dynamic_render.options.clone(); - tree[index].dynamic_render = Some(dynamic_render); - for option in options { - let Some(option) = option else { continue }; - let prev_index = tree[index] - .descendant_range - .is_empty() - .not() - .then_some(tree[index].descendant_range.end - 1); - tree[index].descendant_range.end = tree.len() + 1; - build_tree_item(tree, option, depth + 1, prev_index); - tree[index].total_descendant_range.end = tree.len(); - } - } - SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap { - item: generate_settings_ui_item, - determine_items, - defaults_path, - }) => { - tree[index].generate_items = Some(( - generate_settings_ui_item(), - determine_items, - defaults_path - .into_iter() - .copied() - .map(SharedString::new_static) - .collect(), - )); - } - SettingsUiItem::None => { - return; - } - } -} - -impl SettingsUiTree { - pub fn new(cx: &App) -> Self { - let settings_store = SettingsStore::global(cx); - let mut tree = vec![]; - let mut root_entry_indices = vec![]; - for item in settings_store.settings_ui_items() { - if matches!(item.item, SettingsUiItem::None) - // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to - // link them to other groups - || matches!(item.item, SettingsUiItem::Single(_)) - { - continue; - } - - let prev_root_entry_index = root_entry_indices.last().copied(); - root_entry_indices.push(tree.len()); - build_tree_item(&mut tree, item, 0, prev_root_entry_index); - } - - root_entry_indices.sort_by_key(|i| &tree[*i].title); - - let active_entry_index = root_entry_indices[0]; - Self { - entries: tree, - root_entry_indices, - active_entry_index, - } - } - - // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree, - // so that we can keep none/skip and still test in CI that all settings have - #[cfg(feature = "test-support")] - pub fn all_paths(&self, cx: &App) -> Vec> { - // todo(settings_ui) this needs to be implemented not in terms of JSON anymore - Vec::default() - } -} - -fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context) -> Div { - let mut nav = v_flex().p_4().gap_2(); - for &index in &tree.root_entry_indices { - nav = nav.child( - div() - .id(index) - .on_click(cx.listener(move |settings, _, _, _| { - settings.settings_tree.active_entry_index = index; - })) - .child( - Label::new(tree.entries[index].title.clone()) - .size(LabelSize::Large) - .when(tree.active_entry_index == index, |this| { - this.color(Color::Selected) - }), - ), - ); - } - nav -} - -fn render_content( - tree: &SettingsUiTree, - window: &mut Window, - cx: &mut Context, -) -> Div { - let content = v_flex().size_full().gap_4(); - - let mut path = smallvec::smallvec![]; - - return render_recursive( - &tree.entries, - tree.active_entry_index, - &mut path, - content, - &mut None, - true, - window, - cx, - ); -} - -fn render_recursive( - tree: &[UiEntry], - index: usize, - path: &mut SmallVec<[SharedString; 1]>, - mut element: Div, - fallback_path: &mut Option>, - render_next_title: bool, - window: &mut Window, - cx: &mut App, -) -> Div { - let Some(child) = tree.get(index) else { - return element - .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error)); - }; - - if render_next_title { - element = element.child(Label::new(child.title.clone()).size(LabelSize::Large)); - } - - // todo(settings_ui): subgroups? - let mut pushed_path = false; - if let Some(child_path) = child.path.as_ref() { - path.push(child_path.clone()); - if let Some(fallback_path) = fallback_path.as_mut() { - fallback_path.push(child_path.clone()); - } - pushed_path = true; - } - let settings_value = settings_value_from_settings_and_path( - path.clone(), - fallback_path.as_ref().map(|path| path.as_slice()), - child.title.clone(), - child.documentation.clone(), - // PERF: how to structure this better? There feels like there's a way to avoid the clone - // and every value lookup - SettingsStore::global(cx).raw_user_settings(), - SettingsStore::global(cx).raw_default_settings(), - ); - if let Some(dynamic_render) = child.dynamic_render.as_ref() { - let value = settings_value.read(); - let selected_index = (dynamic_render.determine_option)(value, cx); - element = element.child(div().child(render_toggle_button_group_inner( - settings_value.title.clone(), - dynamic_render.labels, - Some(selected_index), - { - let path = settings_value.path.clone(); - let defaults = dynamic_render.defaults.clone(); - move |idx, cx| { - if idx == selected_index { - return; - } - let default = defaults.get(idx).cloned().unwrap_or_default(); - SettingsValue::write_value(&path, default, cx); - } - }, - ))); - // we don't add descendants for unit options, so we adjust the selected index - // by the number of options we didn't add descendants for, to get the descendant index - let selected_descendant_index = selected_index - - dynamic_render.options[..selected_index] - .iter() - .filter(|option| option.is_none()) - .count(); - if dynamic_render.options[selected_index].is_some() - && let Some(descendant_index) = - child.nth_descendant_index(tree, selected_descendant_index) - { - element = render_recursive( - tree, - descendant_index, - path, - element, - fallback_path, - false, - window, - cx, - ); - } - } else if let Some((settings_ui_item, generate_items, defaults_path)) = - child.generate_items.as_ref() - { - let generated_items = generate_items(settings_value.read(), cx); - let mut ui_items = Vec::with_capacity(generated_items.len()); - for item in generated_items { - let settings_ui_entry = SettingsUiEntry { - path: None, - title: "", - documentation: None, - item: settings_ui_item.clone(), - }; - let prev_index = if ui_items.is_empty() { - None - } else { - Some(ui_items.len() - 1) - }; - let item_index = ui_items.len(); - build_tree_item( - &mut ui_items, - settings_ui_entry, - child._depth + 1, - prev_index, - ); - if item_index < ui_items.len() { - ui_items[item_index].path = None; - ui_items[item_index].title = item.title.clone(); - ui_items[item_index].documentation = item.documentation.clone(); - - // push path instead of setting path on ui item so that the path isn't pushed to default_path as well - // when we recurse - path.push(item.path.clone()); - element = render_recursive( - &ui_items, - item_index, - path, - element, - &mut Some(defaults_path.clone()), - true, - window, - cx, - ); - path.pop(); - } - } - } else if let Some(child_render) = child.render.as_ref() { - element = element.child(div().child(render_item_single( - settings_value, - child_render, - window, - cx, - ))); - } else if let Some(child_index) = child.first_descendant_index() { - let mut index = Some(child_index); - while let Some(sub_child_index) = index { - element = render_recursive( - tree, - sub_child_index, - path, - element, - fallback_path, - true, - window, - cx, - ); - index = tree[sub_child_index].next_sibling; - } - } else { - element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted))) - } - - if pushed_path { - path.pop(); - if let Some(fallback_path) = fallback_path.as_mut() { - fallback_path.pop(); - } - } - return element; -} - -impl Render for SettingsPage { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new()); - div() - .grid() - .grid_cols(16) - .p_4() - .bg(cx.theme().colors().editor_background) - .size_full() - .child( - div() - .id("settings-ui-nav") - .col_span(2) - .h_full() - .child(render_nav(&self.settings_tree, window, cx)), - ) - .child( - div().col_span(6).h_full().child( - render_content(&self.settings_tree, window, cx) - .id("settings-ui-content") - .track_scroll(scroll_handle.read(cx)) - .overflow_y_scroll(), - ), - ) - } -} - -fn element_id_from_path(path: &[SharedString]) -> ElementId { - if path.len() == 0 { - panic!("Path length must not be zero"); - } else if path.len() == 1 { - ElementId::Name(path[0].clone()) - } else { - ElementId::from(( - ElementId::from(path[path.len() - 2].clone()), - path[path.len() - 1].clone(), - )) - } -} - -fn render_item_single( - settings_value: SettingsValue, - item: &SettingsUiItemSingle, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - match item { - SettingsUiItemSingle::Custom(_) => div() - .child(format!("Item: {}", settings_value.path.join("."))) - .into_any_element(), - SettingsUiItemSingle::SwitchField => { - render_any_item(settings_value, render_switch_field, window, cx) - } - SettingsUiItemSingle::NumericStepper(num_type) => { - render_any_numeric_stepper(settings_value, *num_type, window, cx) - } - SettingsUiItemSingle::ToggleGroup { - variants: values, - labels: titles, - } => render_toggle_button_group(settings_value, values, titles, window, cx), - SettingsUiItemSingle::DropDown { variants, labels } => { - render_dropdown(settings_value, variants, labels, window, cx) - } - SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx), - } -} - -pub fn read_settings_value_from_path<'a>( - settings_contents: &'a serde_json::Value, - path: &[impl AsRef], -) -> Option<&'a serde_json::Value> { - // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested - let Some((key, remaining)) = path.split_first() else { - return Some(settings_contents); - }; - let Some(value) = settings_contents.get(key.as_ref()) else { - return None; - }; - - read_settings_value_from_path(value, remaining) -} - -fn downcast_any_item( - settings_value: SettingsValue, -) -> SettingsValue { - let value = settings_value.value.map(|value| { - serde_json::from_value::(value.clone()) - .with_context(|| format!("path: {:?}", settings_value.path.join("."))) - .with_context(|| format!("value is not a {}: {}", std::any::type_name::(), value)) - .unwrap() - }); - // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values - let default_value = serde_json::from_value::(settings_value.default_value) - .with_context(|| format!("path: {:?}", settings_value.path.join("."))) - .with_context(|| format!("value is not a {}", std::any::type_name::())) - .unwrap(); - let deserialized_setting_value = SettingsValue { - title: settings_value.title, - path: settings_value.path, - documentation: settings_value.documentation, - value, - default_value, - }; - deserialized_setting_value -} - -fn render_any_item( - settings_value: SettingsValue, - render_fn: impl Fn(SettingsValue, &mut Window, &mut App) -> AnyElement + 'static, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let deserialized_setting_value = downcast_any_item(settings_value); - render_fn(deserialized_setting_value, window, cx) -} - -fn render_any_numeric_stepper( - settings_value: SettingsValue, - num_type: NumType, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - match num_type { - NumType::U64 => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| u64::saturating_sub(n, 1), - |n| u64::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert u64 to serde_json::Number") - }, - window, - cx, - ), - NumType::U32 => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| u32::saturating_sub(n, 1), - |n| u32::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert u32 to serde_json::Number") - }, - window, - cx, - ), - NumType::F32 => render_numeric_stepper::( - downcast_any_item(settings_value), - |a| a - 1.0, - |a| a + 1.0, - |n| { - serde_json::Number::from_f64(n as f64) - .context("Failed to convert f32 to serde_json::Number") - }, - window, - cx, - ), - NumType::USIZE => render_numeric_stepper::( - downcast_any_item(settings_value), - |n| usize::saturating_sub(n, 1), - |n| usize::saturating_add(n, 1), - |n| { - serde_json::Number::try_from(n) - .context("Failed to convert usize to serde_json::Number") - }, - window, - cx, - ), - NumType::U32NONZERO => render_numeric_stepper::( - downcast_any_item(settings_value), - |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN), - |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX), - |n| { - serde_json::Number::try_from(n.get()) - .context("Failed to convert usize to serde_json::Number") - }, - window, - cx, - ), - } -} - -fn render_numeric_stepper( - value: SettingsValue, - saturating_sub_1: fn(T) -> T, - saturating_add_1: fn(T) -> T, - to_serde_number: fn(T) -> anyhow::Result, - _window: &mut Window, - _cx: &mut App, -) -> AnyElement { - let id = element_id_from_path(&value.path); - let path = value.path.clone(); - let num = *value.read(); - - NumericStepper::new( - id, - num.to_string(), - { - let path = value.path; - move |_, _, cx| { - let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else { - return; - }; - let new_value = serde_json::Value::Number(number); - SettingsValue::write_value(&path, new_value, cx); - } - }, - move |_, _, cx| { - let Some(number) = to_serde_number(saturating_add_1(num)).ok() else { - return; - }; - - let new_value = serde_json::Value::Number(number); - - SettingsValue::write_value(&path, new_value, cx); - }, - ) - .style(ui::NumericStepperStyle::Outlined) - .into_any_element() -} - -fn render_switch_field( - value: SettingsValue, - _window: &mut Window, - _cx: &mut App, -) -> AnyElement { - let id = element_id_from_path(&value.path); - let path = value.path.clone(); - SwitchField::new( - id, - value.title.clone(), - value.documentation.clone(), - match value.read() { - true => ToggleState::Selected, - false => ToggleState::Unselected, - }, - move |toggle_state, _, cx| { - let new_value = serde_json::Value::Bool(match toggle_state { - ToggleState::Indeterminate => { - return; - } - ToggleState::Selected => true, - ToggleState::Unselected => false, - }); - - SettingsValue::write_value(&path, new_value, cx); - }, - ) - .into_any_element() -} - -fn render_text_field( - value: SettingsValue, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let path = value.path.clone(); - let editor = window.use_state(cx, { - let path = path.clone(); - move |window, cx| { - let mut editor = Editor::single_line(window, cx); - - cx.observe_global_in::(window, move |editor, window, cx| { - let user_settings = SettingsStore::global(cx).raw_user_settings(); - if let Some(value) = read_settings_value_from_path(&user_settings, &path).cloned() - && let Some(value) = value.as_str() - { - editor.set_text(value, window, cx); - } - }) - .detach(); - - editor.set_text(value.read().clone(), window, cx); - editor - } - }); - - let weak_editor = editor.downgrade(); - let theme_colors = cx.theme().colors(); - - div() - .child(editor) - .bg(theme_colors.editor_background) - .border_1() - .rounded_lg() - .border_color(theme_colors.border) - .on_action::({ - move |_, _, cx| { - let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok(); - - if let Some(new_value) = new_value { - SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx); - } - } - }) - .into_any_element() -} - -fn render_toggle_button_group( - value: SettingsValue, - variants: &'static [&'static str], - labels: &'static [&'static str], - _: &mut Window, - _: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let active_value = value.read(); - let selected_idx = variants.iter().position(|v| v == &active_value); - - return render_toggle_button_group_inner(value.title, labels, selected_idx, { - let path = value.path.clone(); - move |variant_index, cx| { - SettingsValue::write_value( - &path, - serde_json::Value::String(variants[variant_index].to_string()), - cx, - ); - } - }); -} - -fn render_dropdown( - value: SettingsValue, - variants: &'static [&'static str], - labels: &'static [&'static str], - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let value = downcast_any_item::(value); - let id = element_id_from_path(&value.path); - - let menu = window.use_keyed_state(id.clone(), cx, |window, cx| { - let path = value.path.clone(); - let handler = Rc::new(move |variant: &'static str, cx: &mut App| { - SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx); - }); - - ContextMenu::build(window, cx, |mut menu, _, _| { - for (label, variant) in labels.iter().zip(variants) { - menu = menu.entry(*label, None, { - let handler = handler.clone(); - move |_, cx| { - handler(variant, cx); - } - }); - } - - menu - }) - }); - - DropdownMenu::new(id, value.read(), menu.read(cx).clone()) - .style(ui::DropdownStyle::Outlined) - .into_any_element() -} - -fn render_toggle_button_group_inner( - title: SharedString, - labels: &'static [&'static str], - selected_idx: Option, - on_write: impl Fn(usize, &mut App) + 'static, -) -> AnyElement { - fn make_toggle_group( - title: SharedString, - selected_idx: Option, - on_write: Rc, - labels: &'static [&'static str], - ) -> AnyElement { - let labels_array: [&'static str; LEN] = { - let mut arr = ["unused"; LEN]; - arr.copy_from_slice(labels); - arr - }; - - let mut idx = 0; - ToggleButtonGroup::single_row( - title, - labels_array.map(|label| { - idx += 1; - let on_write = on_write.clone(); - ToggleButtonSimple::new(label, move |_, _, cx| { - on_write(idx - 1, cx); - }) - }), - ) - .when_some(selected_idx, |this, ix| this.selected_index(ix)) - .style(ui::ToggleButtonGroupStyle::Filled) - .into_any_element() - } - - let on_write = Rc::new(on_write); - - macro_rules! templ_toggl_with_const_param { - ($len:expr) => { - if labels.len() == $len { - return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels); - } - }; - } - templ_toggl_with_const_param!(1); - templ_toggl_with_const_param!(2); - templ_toggl_with_const_param!(3); - templ_toggl_with_const_param!(4); - templ_toggl_with_const_param!(5); - templ_toggl_with_const_param!(6); - unreachable!("Too many variants"); -} - -fn settings_value_from_settings_and_path( - path: SmallVec<[SharedString; 1]>, - fallback_path: Option<&[SharedString]>, - title: SharedString, - documentation: Option, - user_settings: &serde_json::Value, - default_settings: &serde_json::Value, -) -> SettingsValue { - let default_value = read_settings_value_from_path(default_settings, &path) - .or_else(|| { - fallback_path.and_then(|fallback_path| { - read_settings_value_from_path(default_settings, fallback_path) - }) - }) - .with_context(|| format!("No default value for item at path {:?}", path.join("."))) - .expect("Default value set for item") - .clone(); - - let value = read_settings_value_from_path(user_settings, &path).cloned(); - let settings_value = SettingsValue { - default_value, - value, - documentation, - path, - // todo(settings_ui) is title required inside SettingsValue? - title, - }; - return settings_value; -} diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 484b9f915e79f726ff346e8f216af420eb6cde45..7a2ae08cace066bd6fa73d9914aac847b0fb1136 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -224,7 +224,6 @@ mod test { use settings::SettingsStore; use crate::{ - VimSettings, state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 8d3fec1d64e96bf675e16bed036e0862a664d840..7dfdb973c7603e9ef28bf757a9a716e729b72170 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -989,11 +989,10 @@ impl Vim { mod test { use gpui::{KeyBinding, TestAppContext, UpdateGlobal}; use indoc::indoc; - use language::language_settings::AllLanguageSettings; use settings::SettingsStore; use crate::{ - VimSettings, motion, + motion, state::Mode::{self}, test::{NeovimBackedTestContext, VimTestContext}, }; diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 04761d3270b9f4ffef8c4e3880ee4e9fb21fd2ad..99e84fcaf1a81f7969baeb1630d36373c6852b53 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -311,17 +311,13 @@ impl Vim { #[cfg(test)] mod test { use crate::{ - UseSystemClipboard, VimSettings, state::{Mode, Register}, test::{NeovimBackedTestContext, VimTestContext}, }; use gpui::ClipboardItem; use indoc::indoc; - use language::{ - LanguageName, - language_settings::{AllLanguageSettings, LanguageSettingsContent}, - }; - use settings::SettingsStore; + use language::{LanguageName, language_settings::LanguageSettingsContent}; + use settings::{SettingsStore, UseSystemClipboard}; #[gpui::test] async fn test_paste(cx: &mut gpui::TestAppContext) { @@ -712,7 +708,7 @@ mod test { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings(cx, |settings| { settings.project.all_languages.languages.0.insert( - LanguageName::new("Rust"), + LanguageName::new("Rust").0, LanguageSettingsContent { auto_indent_on_paste: Some(false), ..Default::default() diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 647f955544ef876647af94f788484a3bc5fbfdfe..8f1a157013000ae4df2416ddca93743f2c926d29 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -271,7 +271,7 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::{EditorSettings, ScrollBeyondLastLine}; + use editor::ScrollBeyondLastLine; use gpui::{AppContext as _, point, px, size}; use indoc::indoc; use language::Point; diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 6ebf8e6c02e94906d973cc1ebcd07726b4ef1ac7..6da18164e42e42b3c1932b094fab5cecf048c155 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -645,7 +645,6 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::EditorSettings; use editor::{DisplayPoint, display_map::DisplayRow}; use indoc::indoc; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 84376719d141fa4862a3e7a1b0f6116dd809bfe5..2256c2577ecd282f690ee7b3afe9e2b21b6e8788 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -22,7 +22,6 @@ pub use vim_test_context::*; use indoc::indoc; use search::BufferSearchBar; -use workspace::WorkspaceSettings; use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode}; @@ -1562,10 +1561,10 @@ async fn test_plus_minus(cx: &mut gpui::TestAppContext) { async fn test_command_alias(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, |s| { + store.update_user_settings(cx, |s| { let mut aliases = HashMap::default(); aliases.insert("Q".to_string(), "upper".to_string()); - s.command_aliases = Some(aliases) + s.workspace.command_aliases = aliases }); }); diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 6c9df164e0fe880c81960a412519347aff5959bd..c36307221ed2d9486455f48effdbbc85e92d4ec0 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -6,7 +6,7 @@ use std::{ panic, thread, }; -use language::language_settings::{AllLanguageSettings, SoftWrap}; +use language::language_settings::SoftWrap; use util::test::marked_text_offsets; use super::{VimTestContext, neovim_connection::NeovimConnection}; @@ -245,9 +245,14 @@ impl NeovimBackedTestContext { self.update(|_, cx| { SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); - settings.defaults.preferred_line_length = Some(columns); + settings.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.soft_wrap = + Some(SoftWrap::PreferredLineLength); + settings + .project + .all_languages + .defaults + .preferred_line_length = Some(columns); }); }) }) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 3e6c96ac72f1e6dee568dafbfda6956c1dd5970c..a2db0493d99190bc7355a5af5a0687befcd02f63 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -68,7 +68,7 @@ impl VimTestContext { pub fn init_keybindings(enabled: bool, cx: &mut App) { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(enabled)); + store.update_user_settings(cx, |s| s.vim_mode = Some(enabled)); }); let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( "keymaps/default-macos.json", @@ -137,7 +137,7 @@ impl VimTestContext { pub fn enable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(true)); + store.update_user_settings(cx, |s| s.vim_mode = Some(true)); }); }) } @@ -145,7 +145,7 @@ impl VimTestContext { pub fn disable_vim(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| s.vim_mode = Some(false)); + store.update_user_settings(cx, |s| s.vim_mode = Some(false)); }); }) } @@ -153,9 +153,7 @@ impl VimTestContext { pub fn enable_helix(&mut self) { self.cx.update(|_, cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |s| { - s.helix_mode = Some(true) - }); + store.update_user_settings(cx, |s| s.helix_mode = Some(true)); }); }) } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 248e6acc58de7056f272d97153ed6342a18667c5..0786745ae5c23832e670232d676d2b84b43c4eed 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -19,7 +19,6 @@ mod state; mod surrounds; mod visual; -use anyhow::Result; use collections::HashMap; use editor::{ Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects, @@ -38,15 +37,15 @@ use normal::search::SearchSubmit; use object::Object; use schemars::JsonSchema; use serde::Deserialize; -use serde::Serialize; -use settings::{ - Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi, update_settings_file, +pub use settings::{ + ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file, }; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use theme::ThemeSettings; use ui::{IntoElement, SharedString, px}; +use util::MergeFrom; use vim_mode_setting::HelixModeSetting; use vim_mode_setting::VimModeSetting; use workspace::{self, Pane, Workspace}; @@ -1793,34 +1792,61 @@ impl Vim { } } -#[derive(Deserialize)] struct VimSettings { pub default_mode: Mode, pub toggle_relative_line_numbers: bool, - pub use_system_clipboard: UseSystemClipboard, + pub use_system_clipboard: settings::UseSystemClipboard, pub use_smartcase_find: bool, pub custom_digraphs: HashMap>, pub highlight_on_yank_duration: u64, pub cursor_shape: CursorShapeSettings, } +/// The settings for cursor shape. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct CursorShapeSettings { + /// Cursor shape for the normal mode. + /// + /// Default: block + pub normal: Option, + /// Cursor shape for the replace mode. + /// + /// Default: underline + pub replace: Option, + /// Cursor shape for the visual mode. + /// + /// Default: block + pub visual: Option, + /// Cursor shape for the insert mode. + /// + /// The default value follows the primary cursor_shape. + pub insert: Option, +} + +impl From for CursorShapeSettings { + fn from(settings: settings::CursorShapeSettings) -> Self { + Self { + normal: settings.normal.map(Into::into), + replace: settings.replace.map(Into::into), + visual: settings.visual.map(Into::into), + insert: settings.insert.map(Into::into), + } + } +} + impl From for Mode { fn from(mode: ModeContent) -> Self { match mode { ModeContent::Normal => Self::Normal, ModeContent::Insert => Self::Insert, - ModeContent::Replace => Self::Replace, - ModeContent::Visual => Self::Visual, - ModeContent::VisualLine => Self::VisualLine, - ModeContent::VisualBlock => Self::VisualBlock, ModeContent::HelixNormal => Self::HelixNormal, } } } impl Settings for VimSettings { - fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self { - let vim = content.vim.as_ref().unwrap(); + fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self { + let vim = content.vim.clone().unwrap(); Self { default_mode: vim.default_mode.unwrap().into(), toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(), @@ -1828,11 +1854,11 @@ impl Settings for VimSettings { use_smartcase_find: vim.use_smartcase_find.unwrap(), custom_digraphs: vim.custom_digraphs.unwrap(), highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), - cursor_shape: vim.cursor_shape.unwrap(), + cursor_shape: vim.cursor_shape.unwrap().into(), } } - fn refine(&mut self, content: &settings::SettingsContent, cx: &mut App) { + fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) { let Some(vim) = content.vim.as_ref() else { return; }; @@ -1846,7 +1872,8 @@ impl Settings for VimSettings { self.custom_digraphs.merge_from(&vim.custom_digraphs); self.highlight_on_yank_duration .merge_from(&vim.highlight_on_yank_duration); - self.cursor_shape.merge_from(&vim.cursor_shape); + self.cursor_shape + .merge_from(&vim.cursor_shape.map(Into::into)); } fn import_from_vscode( diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e53dedafd2f27699645662155f154f1220c6cd85..c3db2c0f9f8189501b4971318f0e7ff1972a89fa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,7 +126,6 @@ serde.workspace = true serde_json.workspace = true session.workspace = true settings.workspace = true -settings_ui.workspace = true keymap_editor.workspace = true shellexpand.workspace = true smol.workspace = true @@ -185,7 +184,6 @@ itertools.workspace = true language = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } -settings_ui = { workspace = true, features = ["test-support"] } terminal_view = { workspace = true, features = ["test-support"] } tree-sitter-md.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 63ee30043b61fd55af2edad7cd4a7a407af743bd..a2aefb47ab6de7296257c21b5cccf283beb30a79 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -624,7 +624,6 @@ pub fn main() { markdown_preview::init(cx); svg_preview::init(cx); onboarding::init(cx); - settings_ui::init(cx); keymap_editor::init(cx); extensions_ui::init(cx); zeta::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5c1168713db687908c0d6a047f3585f920f32b80..6029ce6530a729ccd0b299bdcab2acbd620bbbdd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1948,7 +1948,7 @@ mod tests { }; use language::{LanguageMatcher, LanguageRegistry}; use pretty_assertions::{assert_eq, assert_ne}; - use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings}; + use project::{Project, ProjectPath}; use serde_json::json; use settings::{SettingsStore, watch_config_file}; use std::{ @@ -2253,8 +2253,11 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.session.restore_unsaved_buffers = false + store.update_user_settings(cx, |settings| { + settings + .session + .get_or_insert_default() + .restore_unsaved_buffers = Some(false) }); }); }); @@ -2969,8 +2972,8 @@ mod tests { let app_state = init_test(cx); cx.update(|cx| { cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = + store.update_user_settings(cx, |project_settings| { + project_settings.project.worktree.file_scan_exclusions = Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); }); }); @@ -4799,8 +4802,9 @@ mod tests { // 3. Add .zed to file scan exclusions in user settings cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |worktree_settings| { - worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]); + store.update_user_settings(cx, |worktree_settings| { + worktree_settings.project.worktree.file_scan_exclusions = + Some(vec![".zed".to_string()]); }); }); @@ -4860,34 +4864,4 @@ mod tests { "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" ); } - - #[gpui::test] - fn test_settings_defaults(cx: &mut TestAppContext) { - cx.update(|cx| { - settings::init(cx); - workspace::init_settings(cx); - title_bar::init(cx); - editor::init_settings(cx); - debugger_ui::init(cx); - }); - let default_json = - cx.read(|cx| cx.global::().raw_default_settings().clone()); - - let all_paths = cx.read(|cx| settings_ui::SettingsUiTree::new(cx).all_paths(cx)); - let mut failures = Vec::new(); - for path in all_paths { - if settings_ui::read_settings_value_from_path(&default_json, &path).is_none() { - failures.push(path); - } - } - if !failures.is_empty() { - panic!( - "No default value found for paths: {:#?}", - failures - .into_iter() - .map(|path| path.join(".")) - .collect::>() - ); - } - } } diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 7c8d6fb9f0180b332187e7dbcd32f4b96925b231..0167d878fa34976d7175a64269d9dfe29d18d8fe 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -3,7 +3,7 @@ use std::any::{Any, TypeId}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag}; use gpui::actions; -use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; +use language::language_settings::EditPredictionProvider; use project::DisableAiSettings; use settings::{Settings, SettingsStore, update_settings_file}; use ui::App; diff --git a/crates/zeta_cli/src/headless.rs b/crates/zeta_cli/src/headless.rs index 04d4e9279a5a6a82081078f338e215ca67dca3e8..bb4cb010cba6ea29a9bcd6d8cc0dc93475dbc2a0 100644 --- a/crates/zeta_cli/src/headless.rs +++ b/crates/zeta_cli/src/headless.rs @@ -31,7 +31,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState { release_channel::init(app_version, cx); gpui_tokio::init(cx); - let mut settings_store = SettingsStore::new(cx, settings::default_settings()); + let settings_store = SettingsStore::new(cx, &settings::default_settings()); cx.set_global(settings_store); client::init_settings(cx);