diff --git a/assets/settings/default.json b/assets/settings/default.json index 5041f79305c3a..76dbfb9a17dbf 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1168,7 +1168,10 @@ "use_multiline_find": false, "use_smartcase_find": false, "highlight_on_yank_duration": 200, - "custom_digraphs": {} + "custom_digraphs": {}, + "mode_indicator": { + "location": "right" + } }, // The server to connect to. If the environment variable // ZED_SERVER_URL is set, it will override this setting. diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 8d49a6802c195..c2d153c24c866 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -196,10 +196,11 @@ impl Vim { #[cfg(test)] mod test { + use crate::vim_settings::UseSystemClipboard; use crate::{ state::Mode, test::{NeovimBackedTestContext, VimTestContext}, - UseSystemClipboard, VimSettings, + VimSettings, }; use gpui::ClipboardItem; use indoc::indoc; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index e93eeef40484e..749f0dab73373 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -4,8 +4,9 @@ use std::{fmt::Display, ops::Range, sync::Arc}; use crate::command::command_interceptor; use crate::normal::repeat::Replayer; use crate::surrounds::SurroundsType; +use crate::vim_settings::UseSystemClipboard; use crate::{motion::Motion, object::Object}; -use crate::{UseSystemClipboard, Vim, VimSettings}; +use crate::{Vim, VimSettings}; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; use editor::{Anchor, ClipboardSelection, Editor}; diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9fca9f139eb89..df62cff2f54f6 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -17,9 +17,9 @@ mod replace; mod rewrap; mod state; mod surrounds; +pub mod vim_settings; mod visual; -use anyhow::Result; use collections::HashMap; use editor::{ movement::{self, FindRange}, @@ -34,16 +34,15 @@ use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::search::SearchSubmit; -use schemars::JsonSchema; use serde::Deserialize; -use serde_derive::Serialize; -use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; +use settings::{update_settings_file, Settings, SettingsStore}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use theme::ThemeSettings; use ui::{px, IntoElement, VisualContext}; use vim_mode_setting::VimModeSetting; +use vim_settings::VimSettings; use workspace::{self, Pane, ResizeIntent, Workspace}; use crate::state::ReplayableAction; @@ -1204,45 +1203,3 @@ impl Vim { cx.notify() } } - -/// Controls when to use system clipboard. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum UseSystemClipboard { - /// Don't use system clipboard. - Never, - /// Use system clipboard. - Always, - /// Use system clipboard for yank operations. - OnYank, -} - -#[derive(Deserialize)] -struct VimSettings { - pub toggle_relative_line_numbers: bool, - pub use_system_clipboard: UseSystemClipboard, - pub use_multiline_find: bool, - pub use_smartcase_find: bool, - pub custom_digraphs: HashMap>, - pub highlight_on_yank_duration: u64, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -struct VimSettingsContent { - pub toggle_relative_line_numbers: Option, - pub use_system_clipboard: Option, - pub use_multiline_find: Option, - pub use_smartcase_find: Option, - pub custom_digraphs: Option>>, - pub highlight_on_yank_duration: Option, -} - -impl Settings for VimSettings { - const KEY: Option<&'static str> = Some("vim"); - - type FileContent = VimSettingsContent; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - sources.json_merge() - } -} diff --git a/crates/vim/src/vim_settings.rs b/crates/vim/src/vim_settings.rs new file mode 100644 index 0000000000000..ec6c860dccd9f --- /dev/null +++ b/crates/vim/src/vim_settings.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; + +use anyhow::Result; +use collections::HashMap; +use gpui::AppContext; +use schemars::JsonSchema; +use serde::Deserialize; +use serde_derive::Serialize; +use settings::{Settings, SettingsSources}; + +/// Controls when to use system clipboard. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum UseSystemClipboard { + /// Don't use system clipboard. + Never, + /// Use system clipboard. + Always, + /// Use system clipboard for yank operations. + OnYank, +} + +#[derive(Deserialize)] +pub struct VimSettings { + pub toggle_relative_line_numbers: bool, + pub use_system_clipboard: UseSystemClipboard, + pub use_multiline_find: bool, + pub use_smartcase_find: bool, + pub custom_digraphs: HashMap>, + pub highlight_on_yank_duration: u64, + pub mode_indicator: ModeIndicatorSettings, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct VimSettingsContent { + pub toggle_relative_line_numbers: Option, + pub use_system_clipboard: Option, + pub use_multiline_find: Option, + pub use_smartcase_find: Option, + pub custom_digraphs: Option>>, + pub highlight_on_yank_duration: Option, + pub mode_indicator: Option, +} + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ModeIndicatorLocation { + /// Show the vim mode indicator on the left side of the status bar. + Left, + /// Show the vim mode indicator on the right side of the status bar. + #[default] + Right, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ModeIndicatorSettings { + pub location: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct ModeIndicatorSettingsContent { + /// Whether to show the vim mode indicator to the left or to the right in the status bar. + pub location: Option>, +} + +impl Settings for VimSettings { + const KEY: Option<&'static str> = Some("vim"); + + type FileContent = VimSettingsContent; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + sources.json_merge() + } +} + +impl Settings for ModeIndicatorSettings { + const KEY: Option<&'static str> = Some("mode_indicator"); + + type FileContent = ModeIndicatorSettingsContent; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + sources.json_merge() + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5cff09de0f70f..d905595a92545 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -47,6 +47,7 @@ use terminal_view::terminal_panel::{self, TerminalPanel}; use theme::ActiveTheme; use util::{asset_str, ResultExt}; use uuid::Uuid; +use vim::vim_settings::{ModeIndicatorLocation, VimSettings}; use vim_mode_setting::VimModeSetting; use welcome::{BaseKeymap, MultibufferHint}; use workspace::notifications::NotificationId; @@ -185,10 +186,23 @@ pub fn initialize_workspace( status_bar.add_right_item(inline_completion_button, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(active_toolchain_language, cx); - status_bar.add_right_item(vim_mode_indicator, cx); + match VimSettings::get_global(cx).mode_indicator.location { + Some(ModeIndicatorLocation::Left) => status_bar.add_left_item(vim_mode_indicator, cx), + Some(ModeIndicatorLocation::Right) | None => status_bar.add_right_item(vim_mode_indicator, cx), + } status_bar.add_right_item(cursor_position, cx); }); + let mut current_location = VimSettings::get_global(cx).mode_indicator.location; + cx.observe_global::(move |workspace, cx| { + let new_location = VimSettings::get_global(cx).mode_indicator.location; + if current_location != new_location { + current_location = new_location; + update_vim_mode_indicator_position(workspace, new_location, cx); + } + }) + .detach(); + auto_update_ui::notify_of_any_new_update(cx); let handle = cx.view().downgrade(); @@ -212,6 +226,42 @@ pub fn initialize_workspace( feature_gate_zed_pro_actions(cx); } +fn update_vim_mode_indicator_position( + workspace: &mut Workspace, + new_location: Option, + cx: &mut ViewContext, +) { + if let Some(mode_indicator_position) = workspace + .status_bar() + .read(cx) + .position_of_item::() + { + workspace.status_bar().update(cx, |status_bar, cx| { + if let Some(vim_mode_indicator) = status_bar.item_of_type::() { + status_bar.remove_item_at(mode_indicator_position, cx); + match new_location { + Some(ModeIndicatorLocation::Left) => { + if let (Some(diagnostics_indicator), Some(diagnostics_indicator_position)) = + (status_bar.item_of_type::(), + status_bar.position_of_item::()) + { + status_bar.remove_item_at(diagnostics_indicator_position, cx); + status_bar.add_left_item(vim_mode_indicator, cx); + status_bar.add_left_item(diagnostics_indicator, cx); + } else { + status_bar.add_left_item(vim_mode_indicator, cx) + } + } + Some(ModeIndicatorLocation::Right) | None => { + status_bar.add_right_item(vim_mode_indicator, cx) + } + } + } + }); + cx.notify(); + } +} + fn feature_gate_zed_pro_actions(cx: &mut AppContext) { let zed_pro_actions = [TypeId::of::()]; @@ -3504,6 +3554,113 @@ mod tests { cx.run_until_parked(); } + #[gpui::test] + async fn test_vim_mode_indicator_location_setting(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "file": "contents", + }, + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + project.update(cx, |project, _cx| { + project.languages().add(markdown_language()) + }); + let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); + + let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); + let file = entries[0].clone(); + + let pane = workspace + .read_with(cx, |workspace, _| workspace.active_pane().clone()) + .unwrap(); + + workspace + .update(cx, |w, cx| w.open_path(file.clone(), None, true, cx)) + .unwrap() + .await + .unwrap(); + + workspace + .update(cx, |_, cx| { + pane.update(cx, |pane, cx| { + let editor = pane.active_item().unwrap().downcast::().unwrap(); + assert_eq!(editor.project_path(cx), Some(file.clone())); + let buffer = editor.update(cx, |editor, cx| { + editor.insert("dirt", cx); + editor.buffer().downgrade() + }); + (editor.downgrade(), buffer) + }) + }) + .unwrap(); + + let right_position = cx.read(|cx| { + workspace + .read(cx) + .unwrap() + .status_bar() + .read(cx) + .position_of_item::() + }); + assert!(right_position.is_some()); + println!("right_position: {}", right_position.unwrap()); + + // move the indicator to the left + workspace + .update(cx, |workspace, cx| { + update_vim_mode_indicator_position( + workspace, + Some(ModeIndicatorLocation::Left), + cx, + ); + }) + .unwrap(); + + let left_position = cx.read(|cx| { + workspace + .read(cx) + .unwrap() + .status_bar() + .read(cx) + .position_of_item::() + }); + assert!(left_position.is_some()); + println!("left_position: {}", left_position.unwrap()); + + assert!(left_position.unwrap() < right_position.unwrap()); + + // move the indicator back to the right + workspace + .update(cx, |workspace, cx| { + update_vim_mode_indicator_position( + workspace, + Some(ModeIndicatorLocation::Right), + cx, + ); + }) + .unwrap(); + + let back_to_right_position = cx.read(|cx| { + workspace + .read(cx) + .unwrap() + .status_bar() + .read(cx) + .position_of_item::() + }); + assert!(back_to_right_position.is_some()); + + assert!(back_to_right_position.unwrap() > left_position.unwrap()); + } pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { init_test_with_state(cx, cx.update(AppState::test)) } @@ -3528,6 +3685,7 @@ mod tests { notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); + VimSettings::register(cx); release_channel::init(SemanticVersion::default(), cx); command_palette::init(cx); language::init(cx);