mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 19:02:07 +00:00
Add a mode indicator for vim (#2763)
Release Notes: - vim: add a mode indicator ([#409](https://github.com/zed-industries/community/issues/409)) Now updated screenshots with @iamnbutler <img width="1043" alt="Screenshot 2023-07-25 at 11 11 57" src="https://github.com/zed-industries/zed/assets/94272/8301479a-8b58-42d8-81a1-bc40e1e0a4df"> <img width="1043" alt="Screenshot 2023-07-25 at 11 12 00" src="https://github.com/zed-industries/zed/assets/94272/89c3b8bd-9cbc-4fd7-ad10-dac5538ed3a3"> <img width="1043" alt="Screenshot 2023-07-25 at 11 12 12" src="https://github.com/zed-industries/zed/assets/94272/adc87fe3-a720-4779-853b-df9443407046">
This commit is contained in:
commit
39f02c2b72
9 changed files with 238 additions and 18 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8533,6 +8533,7 @@ dependencies = [
|
|||
"indoc",
|
||||
"itertools",
|
||||
"language",
|
||||
"language_selector",
|
||||
"log",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
|
@ -8542,6 +8543,7 @@ dependencies = [
|
|||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"tokio",
|
||||
"util",
|
||||
"workspace",
|
||||
|
|
|
@ -402,6 +402,7 @@ pub struct StatusBar {
|
|||
pub height: f32,
|
||||
pub item_spacing: f32,
|
||||
pub cursor_position: TextStyle,
|
||||
pub vim_mode_indicator: ContainedText,
|
||||
pub active_language: Interactive<ContainedText>,
|
||||
pub auto_update_progress_message: TextStyle,
|
||||
pub auto_update_done_message: TextStyle,
|
||||
|
|
|
@ -32,6 +32,8 @@ language = { path = "../language" }
|
|||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
theme = { path = "../theme" }
|
||||
language_selector = { path = "../language_selector"}
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
@ -44,3 +46,4 @@ project = { path = "../project", features = ["test-support"] }
|
|||
util = { path = "../util", features = ["test-support"] }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
|
|
58
crates/vim/src/mode_indicator.rs
Normal file
58
crates/vim/src/mode_indicator.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use gpui::{elements::Label, AnyElement, Element, Entity, View, ViewContext};
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
|
||||
use crate::state::Mode;
|
||||
|
||||
pub struct ModeIndicator {
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
impl ModeIndicator {
|
||||
pub fn new(mode: Mode) -> Self {
|
||||
Self { mode }
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
if mode != self.mode {
|
||||
self.mode = mode;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ModeIndicator {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ModeIndicator {
|
||||
fn ui_name() -> &'static str {
|
||||
"ModeIndicatorView"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).workspace.status_bar;
|
||||
// we always choose text to be 12 monospace characters
|
||||
// so that as the mode indicator changes, the rest of the
|
||||
// UI stays still.
|
||||
let text = match self.mode {
|
||||
Mode::Normal => "-- NORMAL --",
|
||||
Mode::Insert => "-- INSERT --",
|
||||
Mode::Visual { line: false } => "-- VISUAL --",
|
||||
Mode::Visual { line: true } => "VISUAL LINE ",
|
||||
};
|
||||
Label::new(text, theme.vim_mode_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.vim_mode_indicator.container)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ModeIndicator {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_active_pane_item: Option<&dyn ItemHandle>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// nothing to do.
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ mod neovim_connection;
|
|||
mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use command_palette::CommandPalette;
|
||||
use editor::DisplayPoint;
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
|
@ -14,7 +16,7 @@ pub use vim_test_context::*;
|
|||
use indoc::indoc;
|
||||
use search::BufferSearchBar;
|
||||
|
||||
use crate::state::Mode;
|
||||
use crate::{state::Mode, ModeIndicator};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
|
@ -195,3 +197,57 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
|
|||
cx.simulate_keystrokes(["shift-n"]);
|
||||
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_status_indicator(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
deterministic: Arc<gpui::executor::Deterministic>,
|
||||
) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let mode_indicator = cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
|
||||
assert!(mode_indicator.is_some());
|
||||
mode_indicator.unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Mode::Normal
|
||||
);
|
||||
|
||||
// shows the correct mode
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Mode::Insert
|
||||
);
|
||||
|
||||
// shows even in search
|
||||
cx.simulate_keystrokes(["escape", "v", "/"]);
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
|
||||
Mode::Visual { line: false }
|
||||
);
|
||||
|
||||
// hides if vim mode is disabled
|
||||
cx.disable_vim();
|
||||
deterministic.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
|
||||
assert!(mode_indicator.is_none());
|
||||
});
|
||||
|
||||
cx.enable_vim();
|
||||
deterministic.run_until_parked();
|
||||
cx.workspace(|workspace, cx| {
|
||||
let status_bar = workspace.status_bar().read(cx);
|
||||
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
|
||||
assert!(mode_indicator.is_some());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ mod test;
|
|||
|
||||
mod editor_events;
|
||||
mod insert;
|
||||
mod mode_indicator;
|
||||
mod motion;
|
||||
mod normal;
|
||||
mod object;
|
||||
|
@ -18,6 +19,7 @@ use gpui::{
|
|||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::CursorShape;
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
|
@ -119,6 +121,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
|||
pub struct Vim {
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
editor_subscription: Option<Subscription>,
|
||||
mode_indicator: Option<ViewHandle<ModeIndicator>>,
|
||||
|
||||
enabled: bool,
|
||||
state: VimState,
|
||||
|
@ -178,6 +181,10 @@ impl Vim {
|
|||
self.state.mode = mode;
|
||||
self.state.operator_stack.clear();
|
||||
|
||||
if let Some(mode_indicator) = &self.mode_indicator {
|
||||
mode_indicator.update(cx, |mode_indicator, cx| mode_indicator.set_mode(mode, cx))
|
||||
}
|
||||
|
||||
// Sync editor settings like clip mode
|
||||
self.sync_vim_settings(cx);
|
||||
|
||||
|
@ -264,6 +271,44 @@ impl Vim {
|
|||
}
|
||||
}
|
||||
|
||||
fn sync_mode_indicator(cx: &mut WindowContext) {
|
||||
let Some(workspace) = cx.root_view()
|
||||
.downcast_ref::<Workspace>()
|
||||
.map(|workspace| workspace.downgrade()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
let current_position = status_bar.position_of_item::<ModeIndicator>();
|
||||
|
||||
if vim.enabled && current_position.is_none() {
|
||||
if vim.mode_indicator.is_none() {
|
||||
vim.mode_indicator =
|
||||
Some(cx.add_view(|_| ModeIndicator::new(vim.state.mode)));
|
||||
};
|
||||
let mode_indicator = vim.mode_indicator.as_ref().unwrap();
|
||||
let position = status_bar
|
||||
.position_of_item::<language_selector::ActiveBufferLanguage>();
|
||||
if let Some(position) = position {
|
||||
status_bar.insert_item_after(position, mode_indicator.clone(), cx)
|
||||
} else {
|
||||
status_bar.add_left_item(mode_indicator.clone(), cx)
|
||||
}
|
||||
} else if !vim.enabled {
|
||||
if let Some(position) = current_position {
|
||||
status_bar.remove_item_at(position, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
|
@ -314,6 +359,8 @@ impl Vim {
|
|||
self.unhook_vim_settings(editor, cx);
|
||||
}
|
||||
});
|
||||
|
||||
Vim::sync_mode_indicator(cx);
|
||||
}
|
||||
|
||||
fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
|
|
|
@ -27,6 +27,7 @@ trait StatusItemViewHandle {
|
|||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut WindowContext,
|
||||
);
|
||||
fn ui_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
pub struct StatusBar {
|
||||
|
@ -57,7 +58,6 @@ impl View for StatusBar {
|
|||
.with_margin_right(theme.item_spacing)
|
||||
}))
|
||||
.into_any(),
|
||||
|
||||
right: Flex::row()
|
||||
.with_children(self.right_items.iter().rev().map(|i| {
|
||||
ChildView::new(i.as_any(), cx)
|
||||
|
@ -96,6 +96,56 @@ impl StatusBar {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
|
||||
self.left_items
|
||||
.iter()
|
||||
.chain(self.right_items.iter())
|
||||
.find_map(|item| item.as_any().clone().downcast())
|
||||
}
|
||||
|
||||
pub fn position_of_item<T>(&self) -> Option<usize>
|
||||
where
|
||||
T: StatusItemView,
|
||||
{
|
||||
for (index, item) in self.left_items.iter().enumerate() {
|
||||
if item.as_ref().ui_name() == T::ui_name() {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
for (index, item) in self.right_items.iter().enumerate() {
|
||||
if item.as_ref().ui_name() == T::ui_name() {
|
||||
return Some(index + self.left_items.len());
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn insert_item_after<T>(
|
||||
&mut self,
|
||||
position: usize,
|
||||
item: ViewHandle<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
T: 'static + StatusItemView,
|
||||
{
|
||||
if position < self.left_items.len() {
|
||||
self.left_items.insert(position + 1, Box::new(item))
|
||||
} else {
|
||||
self.right_items
|
||||
.insert(position + 1 - self.left_items.len(), Box::new(item))
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
|
||||
if position < self.left_items.len() {
|
||||
self.left_items.remove(position);
|
||||
} else {
|
||||
self.right_items.remove(position - self.left_items.len());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
T: 'static + StatusItemView,
|
||||
|
@ -133,6 +183,10 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
|
|||
this.set_active_pane_item(active_pane_item, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn ui_name(&self) -> &'static str {
|
||||
T::ui_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn StatusItemViewHandle> for AnyViewHandle {
|
||||
|
|
|
@ -315,6 +315,7 @@ pub fn initialize_workspace(
|
|||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
status_bar.add_left_item(activity_indicator, cx);
|
||||
|
||||
status_bar.add_right_item(feedback_button, cx);
|
||||
status_bar.add_right_item(copilot, cx);
|
||||
status_bar.add_right_item(active_buffer_language, cx);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { background, border, foreground, text } from "./components"
|
||||
import { interactive, toggleable } from "../element"
|
||||
import { useTheme } from "../common"
|
||||
import { text_button } from "../component/text_button"
|
||||
|
||||
export default function status_bar(): any {
|
||||
const theme = useTheme()
|
||||
|
||||
|
@ -26,20 +28,16 @@ export default function status_bar(): any {
|
|||
right: 6,
|
||||
},
|
||||
border: border(layer, { top: true, overlay: true }),
|
||||
cursor_position: text(layer, "sans", "variant"),
|
||||
active_language: interactive({
|
||||
base: {
|
||||
padding: { left: 6, right: 6 },
|
||||
...text(layer, "sans", "variant"),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "sans", "on"),
|
||||
},
|
||||
},
|
||||
cursor_position: text(layer, "sans", "variant", { size: "xs" }),
|
||||
vim_mode_indicator: {
|
||||
margin: { left: 6 },
|
||||
...text(layer, "mono", "variant", { size: "xs" }),
|
||||
},
|
||||
active_language: text_button({
|
||||
color: "variant"
|
||||
}),
|
||||
auto_update_progress_message: text(layer, "sans", "variant"),
|
||||
auto_update_done_message: text(layer, "sans", "variant"),
|
||||
auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }),
|
||||
auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }),
|
||||
lsp_status: interactive({
|
||||
base: {
|
||||
...diagnostic_status_container,
|
||||
|
@ -59,9 +57,9 @@ export default function status_bar(): any {
|
|||
}),
|
||||
diagnostic_message: interactive({
|
||||
base: {
|
||||
...text(layer, "sans"),
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
},
|
||||
state: { hovered: text(layer, "sans", "hovered") },
|
||||
state: { hovered: text(layer, "sans", "hovered", { size: "xs" }) },
|
||||
}),
|
||||
diagnostic_summary: interactive({
|
||||
base: {
|
||||
|
@ -117,7 +115,7 @@ export default function status_bar(): any {
|
|||
icon_color: foreground(layer, "variant"),
|
||||
label: {
|
||||
margin: { left: 6 },
|
||||
...text(layer, "sans", { size: "sm" }),
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
},
|
||||
},
|
||||
state: {
|
||||
|
|
Loading…
Reference in a new issue