sidebar: Keyboard nav improvements (and vim mode) (#51856)
This commit is contained in:
1 parent
fdb769c132
commit
c842fb55dd
12 files changed
+203
-42
No files matched your search
Generated
+1
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user