Cargo.lock π
@@ -15901,6 +15901,7 @@ dependencies = [
"theme",
"ui",
"util",
+ "vim_mode_setting",
"workspace",
"zed_actions",
]
Cameron Mcloughlin created
Cargo.lock | 1
assets/keymaps/default-linux.json | 4
assets/keymaps/default-macos.json | 4
assets/keymaps/default-windows.json | 4
assets/keymaps/vim.json | 27 ++++++
crates/sidebar/Cargo.toml | 1
crates/sidebar/src/sidebar.rs | 111 ++++++++++++++++++++++----
crates/vim/src/test/vim_test_context.rs | 1
crates/vim/src/vim.rs | 2
crates/workspace/src/multi_workspace.rs | 5 +
crates/workspace/src/workspace.rs | 73 +++++++++++++----
crates/zed_actions/src/lib.rs | 12 ++
12 files changed, 203 insertions(+), 42 deletions(-)
@@ -15901,6 +15901,7 @@ dependencies = [
"theme",
"ui",
"util",
+ "vim_mode_setting",
"workspace",
"zed_actions",
]
@@ -675,8 +675,8 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agents_sidebar::NewThreadInGroup",
- "left": "agents_sidebar::CollapseSelectedEntry",
- "right": "agents_sidebar::ExpandSelectedEntry",
+ "left": "menu::SelectParent",
+ "right": "menu::SelectChild",
"enter": "menu::Confirm",
"space": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
@@ -743,8 +743,8 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agents_sidebar::NewThreadInGroup",
- "left": "agents_sidebar::CollapseSelectedEntry",
- "right": "agents_sidebar::ExpandSelectedEntry",
+ "left": "menu::SelectParent",
+ "right": "menu::SelectChild",
"enter": "menu::Confirm",
"space": "menu::Confirm",
"cmd-f": "agents_sidebar::FocusSidebarFilter",
@@ -679,8 +679,8 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agents_sidebar::NewThreadInGroup",
- "left": "agents_sidebar::CollapseSelectedEntry",
- "right": "agents_sidebar::ExpandSelectedEntry",
+ "left": "menu::SelectParent",
+ "right": "menu::SelectChild",
"enter": "menu::Confirm",
"space": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
@@ -1113,4 +1113,31 @@
"k": "notebook::NotebookMoveUp",
},
},
+ {
+ "context": "ThreadsSidebar && !Editor",
+ "bindings": {
+ "j": "menu::SelectNext",
+ "k": "menu::SelectPrevious",
+ "h": "menu::SelectParent",
+ "l": "menu::SelectChild",
+ "g g": "menu::SelectFirst",
+ "shift-g": "menu::SelectLast",
+ "/": "agents_sidebar::FocusSidebarFilter",
+ "z a": "editor::ToggleFold",
+ "z c": "menu::SelectParent",
+ "z o": "menu::SelectChild",
+ "z shift-m": "editor::FoldAll",
+ "z shift-r": "editor::UnfoldAll",
+ },
+ },
+ {
+ "context": "ThreadsSidebar > Editor && VimControl && vim_mode == normal",
+ "bindings": {
+ "j": "editor::MoveDown",
+ "k": "editor::MoveUp",
+ "/": "vim::SwitchToInsertMode",
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ },
+ },
]
@@ -33,6 +33,7 @@ settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
+vim_mode_setting.workspace = true
workspace.workspace = true
zed_actions.workspace = true
@@ -12,13 +12,16 @@ use editor::Editor;
use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
use gpui::{
Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, ListState, Pixels,
- Render, SharedString, WeakEntity, Window, WindowHandle, actions, list, prelude::*, px,
+ Render, SharedString, WeakEntity, Window, WindowHandle, list, prelude::*, px,
+};
+use menu::{
+ Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
};
-use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::{AgentId, Event as ProjectEvent};
use recent_projects::RecentProjects;
use ui::utils::platform_title_bar_height;
+use settings::Settings as _;
use std::collections::{HashMap, HashSet};
use std::mem;
use std::path::Path;
@@ -39,15 +42,11 @@ use workspace::{
use zed_actions::OpenRecent;
use zed_actions::editor::{MoveDown, MoveUp};
-actions!(
+use zed_actions::agents_sidebar::FocusSidebarFilter;
+
+gpui::actions!(
agents_sidebar,
[
- /// Collapses the selected entry in the workspace sidebar.
- CollapseSelectedEntry,
- /// Expands the selected entry in the workspace sidebar.
- ExpandSelectedEntry,
- /// Moves focus to the sidebar's search/filter editor.
- FocusSidebarFilter,
/// Creates a new thread in the currently selected or active project group.
NewThreadInGroup,
]
@@ -264,6 +263,7 @@ impl Sidebar {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
+ editor.set_use_modal_editing(true);
editor.set_placeholder_text("Searchβ¦", window, cx);
editor
});
@@ -1511,6 +1511,16 @@ impl Sidebar {
) {
self.selection = None;
self.filter_editor.focus_handle(cx).focus(window, cx);
+
+ // When vim mode is active, the editor defaults to normal mode which
+ // blocks text input. Switch to insert mode so the user can type
+ // immediately.
+ if vim_mode_setting::VimModeSetting::get_global(cx).0 {
+ if let Ok(action) = cx.build_action("vim::SwitchToInsertMode", None) {
+ window.dispatch_action(action, cx);
+ }
+ }
+
cx.notify();
}
@@ -1894,7 +1904,7 @@ impl Sidebar {
fn expand_selected_entry(
&mut self,
- _: &ExpandSelectedEntry,
+ _: &SelectChild,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1918,7 +1928,7 @@ impl Sidebar {
fn collapse_selected_entry(
&mut self,
- _: &CollapseSelectedEntry,
+ _: &SelectParent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1951,6 +1961,68 @@ impl Sidebar {
}
}
+ fn toggle_selected_fold(
+ &mut self,
+ _: &editor::actions::ToggleFold,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(ix) = self.selection else { return };
+
+ // Find the group header for the current selection.
+ let header_ix = match self.contents.entries.get(ix) {
+ Some(ListEntry::ProjectHeader { .. }) => Some(ix),
+ Some(
+ ListEntry::Thread(_) | ListEntry::ViewMore { .. } | ListEntry::NewThread { .. },
+ ) => (0..ix).rev().find(|&i| {
+ matches!(
+ self.contents.entries.get(i),
+ Some(ListEntry::ProjectHeader { .. })
+ )
+ }),
+ None => None,
+ };
+
+ if let Some(header_ix) = header_ix {
+ if let Some(ListEntry::ProjectHeader { path_list, .. }) =
+ self.contents.entries.get(header_ix)
+ {
+ let path_list = path_list.clone();
+ if self.collapsed_groups.contains(&path_list) {
+ self.collapsed_groups.remove(&path_list);
+ } else {
+ self.selection = Some(header_ix);
+ self.collapsed_groups.insert(path_list);
+ }
+ self.update_entries(false, cx);
+ }
+ }
+ }
+
+ fn fold_all(
+ &mut self,
+ _: &editor::actions::FoldAll,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ for entry in &self.contents.entries {
+ if let ListEntry::ProjectHeader { path_list, .. } = entry {
+ self.collapsed_groups.insert(path_list.clone());
+ }
+ }
+ self.update_entries(false, cx);
+ }
+
+ fn unfold_all(
+ &mut self,
+ _: &editor::actions::UnfoldAll,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.collapsed_groups.clear();
+ self.update_entries(false, cx);
+ }
+
fn stop_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
return;
@@ -2670,6 +2742,9 @@ impl Render for Sidebar {
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::expand_selected_entry))
.on_action(cx.listener(Self::collapse_selected_entry))
+ .on_action(cx.listener(Self::toggle_selected_fold))
+ .on_action(cx.listener(Self::fold_all))
+ .on_action(cx.listener(Self::unfold_all))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::remove_selected_thread))
.on_action(cx.listener(Self::new_thread_in_group))
@@ -3654,7 +3729,7 @@ mod tests {
sidebar.selection = Some(0);
});
- cx.dispatch_action(CollapseSelectedEntry);
+ cx.dispatch_action(SelectParent);
cx.run_until_parked();
assert_eq!(
@@ -3663,7 +3738,7 @@ mod tests {
);
// Press right to expand
- cx.dispatch_action(ExpandSelectedEntry);
+ cx.dispatch_action(SelectChild);
cx.run_until_parked();
assert_eq!(
@@ -3676,7 +3751,7 @@ mod tests {
);
// Press right again on already-expanded header moves selection down
- cx.dispatch_action(ExpandSelectedEntry);
+ cx.dispatch_action(SelectChild);
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
}
@@ -3709,7 +3784,7 @@ mod tests {
);
// Pressing left on a child collapses the parent group and selects it
- cx.dispatch_action(CollapseSelectedEntry);
+ cx.dispatch_action(SelectParent);
cx.run_until_parked();
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
@@ -3773,7 +3848,7 @@ mod tests {
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(2));
// Collapse the group, which removes the thread from the list
- cx.dispatch_action(CollapseSelectedEntry);
+ cx.dispatch_action(SelectParent);
cx.run_until_parked();
// Selection should be clamped to the last valid index (0 = header)
@@ -4383,12 +4458,12 @@ mod tests {
cx.run_until_parked();
// User focuses the sidebar and collapses the group using keyboard:
- // manually select the header, then press CollapseSelectedEntry to collapse.
+ // manually select the header, then press SelectParent to collapse.
open_and_focus_sidebar(&sidebar, cx);
sidebar.update_in(cx, |sidebar, _window, _cx| {
sidebar.selection = Some(0);
});
- cx.dispatch_action(CollapseSelectedEntry);
+ cx.dispatch_action(SelectParent);
cx.run_until_parked();
assert_eq!(
@@ -30,6 +30,7 @@ impl VimTestContext {
theme::init(theme::LoadThemes::JustBase, cx);
settings_ui::init(cx);
markdown_preview::init(cx);
+ zed_actions::init();
});
}
@@ -635,7 +635,7 @@ impl Vim {
fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context<Editor>) {
let vim = Vim::new(window, cx);
let state = vim.update(cx, |vim, cx| {
- if !editor.mode().is_full() {
+ if !editor.use_modal_editing() {
vim.mode = Mode::Insert;
}
@@ -241,9 +241,11 @@ impl MultiWorkspace {
pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
self.sidebar_open = true;
+ let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
for workspace in &self.workspaces {
workspace.update(cx, |workspace, cx| {
workspace.set_workspace_sidebar_open(true, cx);
+ workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
});
}
self.serialize(cx);
@@ -255,6 +257,7 @@ impl MultiWorkspace {
for workspace in &self.workspaces {
workspace.update(cx, |workspace, cx| {
workspace.set_workspace_sidebar_open(false, cx);
+ workspace.set_sidebar_focus_handle(None);
});
}
let pane = self.workspace().read(cx).active_pane().clone();
@@ -349,8 +352,10 @@ impl MultiWorkspace {
index
} else {
if self.sidebar_open {
+ let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
workspace.update(cx, |workspace, cx| {
workspace.set_workspace_sidebar_open(true, cx);
+ workspace.set_sidebar_focus_handle(sidebar_focus_handle);
});
}
Self::subscribe_to_workspace(&workspace, cx);
@@ -1340,6 +1340,7 @@ pub struct Workspace {
last_open_dock_positions: Vec<DockPosition>,
removing: bool,
_panels_task: Option<Task<Result<()>>>,
+ sidebar_focus_handle: Option<FocusHandle>,
}
impl EventEmitter<Event> for Workspace {}
@@ -1745,6 +1746,7 @@ impl Workspace {
scheduled_tasks: Vec::new(),
last_open_dock_positions: Vec::new(),
removing: false,
+ sidebar_focus_handle: None,
}
}
@@ -2163,6 +2165,10 @@ impl Workspace {
});
}
+ pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
+ self.sidebar_focus_handle = handle;
+ }
+
pub fn status_bar_visible(&self, cx: &App) -> bool {
StatusBarSettings::get_global(cx).show
}
@@ -4481,26 +4487,35 @@ impl Workspace {
) {
use ActivateInDirectionTarget as Target;
enum Origin {
+ Sidebar,
LeftDock,
RightDock,
BottomDock,
Center,
}
- let origin: Origin = [
- (&self.left_dock, Origin::LeftDock),
- (&self.right_dock, Origin::RightDock),
- (&self.bottom_dock, Origin::BottomDock),
- ]
- .into_iter()
- .find_map(|(dock, origin)| {
- if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
- Some(origin)
- } else {
- None
- }
- })
- .unwrap_or(Origin::Center);
+ let origin: Origin = if self
+ .sidebar_focus_handle
+ .as_ref()
+ .is_some_and(|h| h.contains_focused(window, cx))
+ {
+ Origin::Sidebar
+ } else {
+ [
+ (&self.left_dock, Origin::LeftDock),
+ (&self.right_dock, Origin::RightDock),
+ (&self.bottom_dock, Origin::BottomDock),
+ ]
+ .into_iter()
+ .find_map(|(dock, origin)| {
+ if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
+ Some(origin)
+ } else {
+ None
+ }
+ })
+ .unwrap_or(Origin::Center)
+ };
let get_last_active_pane = || {
let pane = self
@@ -4519,7 +4534,20 @@ impl Workspace {
let try_dock =
|dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
+ let sidebar_target = self
+ .sidebar_focus_handle
+ .as_ref()
+ .map(|h| Target::Sidebar(h.clone()));
+
let target = match (origin, direction) {
+ // From the sidebar, only Right navigates into the workspace.
+ (Origin::Sidebar, SplitDirection::Right) => try_dock(&self.left_dock)
+ .or_else(|| get_last_active_pane().map(Target::Pane))
+ .or_else(|| try_dock(&self.bottom_dock))
+ .or_else(|| try_dock(&self.right_dock)),
+
+ (Origin::Sidebar, _) => None,
+
// We're in the center, so we first try to go to a different pane,
// otherwise try to go to a dock.
(Origin::Center, direction) => {
@@ -4529,7 +4557,7 @@ impl Workspace {
match direction {
SplitDirection::Up => None,
SplitDirection::Down => try_dock(&self.bottom_dock),
- SplitDirection::Left => try_dock(&self.left_dock),
+ SplitDirection::Left => try_dock(&self.left_dock).or(sidebar_target),
SplitDirection::Right => try_dock(&self.right_dock),
}
}
@@ -4543,18 +4571,24 @@ impl Workspace {
}
}
+ (Origin::LeftDock, SplitDirection::Left) => sidebar_target,
+
(Origin::LeftDock, SplitDirection::Down)
| (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
(Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
- (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
+ (Origin::BottomDock, SplitDirection::Left) => {
+ try_dock(&self.left_dock).or(sidebar_target)
+ }
(Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
(Origin::RightDock, SplitDirection::Left) => {
if let Some(last_active_pane) = get_last_active_pane() {
Some(Target::Pane(last_active_pane))
} else {
- try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
+ try_dock(&self.bottom_dock)
+ .or_else(|| try_dock(&self.left_dock))
+ .or(sidebar_target)
}
}
@@ -4583,6 +4617,9 @@ impl Workspace {
}
})
}
+ Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
+ focus_handle.focus(window, cx);
+ }
None => {}
}
}
@@ -7488,9 +7525,11 @@ fn open_items(
})
}
+#[derive(Clone)]
enum ActivateInDirectionTarget {
Pane(Entity<Pane>),
Dock(Entity<Dock>),
+ Sidebar(FocusHandle),
}
fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
@@ -776,6 +776,18 @@ pub mod preview {
}
}
+pub mod agents_sidebar {
+ use gpui::actions;
+
+ actions!(
+ agents_sidebar,
+ [
+ /// Moves focus to the sidebar's search/filter editor.
+ FocusSidebarFilter,
+ ]
+ );
+}
+
pub mod notebook {
use gpui::actions;