From d08b72b872f3a1c8a167e56100cf0d6ace0a9eb4 Mon Sep 17 00:00:00 2001 From: AidanV Date: Sun, 25 May 2025 23:47:08 -0700 Subject: [PATCH] project_panel vim counts and shortcuts --- Cargo.lock | 1 + assets/keymaps/vim.json | 26 ++++++++-- crates/gpui/src/elements/uniform_list.rs | 54 +++++++++++++++----- crates/project_panel/src/project_panel.rs | 61 ++++++++++++++++++++++- crates/vim/Cargo.toml | 1 + crates/vim/src/vim.rs | 43 ++++++++++++++++ 6 files changed, 169 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76f8672d4d..b83964092a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17937,6 +17937,7 @@ dependencies = [ "language", "log", "lsp", + "menu", "multi_buffer", "nvim-rs", "parking_lot", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index be6d34a134..88deeb2182 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -733,6 +733,21 @@ "escape": "buffer_search::Dismiss" } }, + { + "context": "(GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel) && not_editing", + "bindings": { + "0": ["vim::Number", 0], + "1": ["vim::Number", 1], + "2": ["vim::Number", 2], + "3": ["vim::Number", 3], + "4": ["vim::Number", 4], + "5": ["vim::Number", 5], + "6": ["vim::Number", 6], + "7": ["vim::Number", 7], + "8": ["vim::Number", 8], + "9": ["vim::Number", 9] + } + }, { "context": "VimControl || !Editor && !Terminal", "bindings": { @@ -809,8 +824,8 @@ "enter": "project_panel::OpenPermanent", "escape": "project_panel::ToggleFocus", "h": "project_panel::CollapseSelectedEntry", - "j": "menu::SelectNext", - "k": "menu::SelectPrevious", + "j": "vim::MenuSelectNext", + "k": "vim::MenuSelectPrevious", "l": "project_panel::ExpandSelectedEntry", "o": "project_panel::OpenPermanent", "shift-d": "project_panel::Delete", @@ -829,7 +844,12 @@ "{": "project_panel::SelectPrevDirectory", "shift-g": "menu::SelectLast", "g g": "menu::SelectFirst", - "-": "project_panel::SelectParent" + "-": "project_panel::SelectParent", + "ctrl-u": "project_panel::ScrollUp", + "ctrl-d": "project_panel::ScrollDown", + "z t": "project_panel::ScrollCursorTop", + "z z": "project_panel::ScrollCursorCenter", + "z b": "project_panel::ScrollCursorBottom" } }, { diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index cdf90d4eb8..86b7f4e819 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -88,6 +88,10 @@ pub enum ScrollStrategy { /// May not be possible if there's not enough list items above the item scrolled to: /// in this case, the element will be placed at the closest possible position. Center, + /// Attempt to place the element at the bottom of the list's viewport. + /// May not be possible if there's not enough list items above the item scrolled to: + /// in this case, the element will be placed at the closest possible position. + Bottom, } #[derive(Clone, Copy, Debug)] @@ -99,6 +103,7 @@ pub struct DeferredScrollToItem { pub strategy: ScrollStrategy, /// The offset in number of items pub offset: usize, + pub scroll_strict: bool, } #[derive(Clone, Debug, Default)] @@ -133,12 +138,23 @@ impl UniformListScrollHandle { }))) } - /// Scroll the list to the given item index. + /// Scroll the list so that the given item index is onscreen. pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { item_index: ix, strategy, offset: 0, + scroll_strict: false, + }); + } + + /// Scroll the list so that the given item index is at scroll strategy position. + pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + scroll_strict: true, }); } @@ -152,6 +168,7 @@ impl UniformListScrollHandle { item_index: ix, strategy, offset, + scroll_strict: false, }); } @@ -368,24 +385,35 @@ impl Element for UniformList { updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; } - match deferred_scroll.strategy { - ScrollStrategy::Top => {} - ScrollStrategy::Center => { - if scrolled_to_top { + if deferred_scroll.scroll_strict + || (scrolled_to_top + && (item_top < scroll_top + offset_pixels + || item_bottom > scroll_top + list_height)) + { + match deferred_scroll.strategy { + ScrollStrategy::Top => { + updated_scroll_offset.y = -item_top + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); + } + ScrollStrategy::Center => { let item_center = item_top + item_height / 2.0; let viewport_height = list_height - offset_pixels; let viewport_center = offset_pixels + viewport_height / 2.0; let target_scroll_top = item_center - viewport_center; - if item_top < scroll_top + offset_pixels - || item_bottom > scroll_top + list_height - { - updated_scroll_offset.y = -target_scroll_top - .max(Pixels::ZERO) - .min(content_height - list_height) - .max(Pixels::ZERO); - } + updated_scroll_offset.y = -target_scroll_top + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); + } + ScrollStrategy::Bottom => { + updated_scroll_offset.y = -(item_bottom - list_height) + .max(Pixels::ZERO) + .min(content_height - list_height) + .max(Pixels::ZERO); } } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 52ec7a9880..23a8abdafb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -87,6 +87,7 @@ pub struct ProjectPanel { // An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's // hovered over the start/end of a list. hover_scroll_task: Option>, + rendered_entries_len: usize, visible_entries: Vec, /// Maps from leaf project entry ID to the currently selected ancestor. /// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several @@ -277,6 +278,11 @@ actions!( UnfoldDirectory, /// Folds the selected directory. FoldDirectory, + ScrollUp, + ScrollDown, + ScrollCursorCenter, + ScrollCursorTop, + ScrollCursorBottom, /// Selects the parent directory. SelectParent, /// Selects the next entry with git changes. @@ -603,6 +609,7 @@ impl ProjectPanel { hover_scroll_task: None, fs: workspace.app_state().fs.clone(), focus_handle, + rendered_entries_len: 0, visible_entries: Default::default(), ancestors: Default::default(), folded_directory_drag_target: None, @@ -1989,6 +1996,52 @@ impl ProjectPanel { } } + fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context) { + for _ in 0..self.rendered_entries_len / 2 { + window.dispatch_action(SelectPrevious.boxed_clone(), cx); + } + } + + fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context) { + for _ in 0..self.rendered_entries_len / 2 { + window.dispatch_action(SelectNext.boxed_clone(), cx); + } + } + + fn scroll_cursor_center( + &mut self, + _: &ScrollCursorCenter, + _: &mut Window, + cx: &mut Context, + ) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Center); + cx.notify(); + } + } + + fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, _: &mut Window, cx: &mut Context) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Top); + cx.notify(); + } + } + + fn scroll_cursor_bottom( + &mut self, + _: &ScrollCursorBottom, + _: &mut Window, + cx: &mut Context, + ) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.scroll_handle + .scroll_to_item_strict(index, ScrollStrategy::Bottom); + cx.notify(); + } + } + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { if let Some(edit_state) = &self.edit_state && edit_state.processing_filename.is_none() @@ -5233,6 +5286,11 @@ impl Render for ProjectPanel { this.marked_entries.clear(); })) .key_context(self.dispatch_context(window, cx)) + .on_action(cx.listener(Self::scroll_up)) + .on_action(cx.listener(Self::scroll_down)) + .on_action(cx.listener(Self::scroll_cursor_center)) + .on_action(cx.listener(Self::scroll_cursor_top)) + .on_action(cx.listener(Self::scroll_cursor_bottom)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) @@ -5313,7 +5371,8 @@ impl Render for ProjectPanel { .child( uniform_list("entries", item_count, { cx.processor(|this, range: Range, window, cx| { - let mut items = Vec::with_capacity(range.end - range.start); + this.rendered_entries_len = range.end - range.start; + let mut items = Vec::with_capacity(this.rendered_entries_len); this.for_each_visible_entry( range, window, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 434b14b07c..8c3348f003 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -44,6 +44,7 @@ settings.workspace = true task.workspace = true text.workspace = true theme.workspace = true +menu.workspace = true tokio = { version = "1.15", features = ["full"], optional = true } ui.workspace = true util.workspace = true diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9da01e6f44..3cd2b28d20 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -222,6 +222,8 @@ actions!( PushReplaceWithRegister, /// Toggles comments. PushToggleComments, + MenuSelectNext, + MenuSelectPrevious ] ); @@ -251,6 +253,47 @@ pub fn init(cx: &mut App) { }) }); + workspace.register_action(|_, _: &MenuSelectNext, window, cx| { + let count = Vim::take_count(cx).unwrap_or(1); + + for _ in 0..count { + window.dispatch_action(menu::SelectNext.boxed_clone(), cx); + } + }); + + workspace.register_action(|_, _: &MenuSelectPrevious, window, cx| { + let count = Vim::take_count(cx).unwrap_or(1); + + for _ in 0..count { + window.dispatch_action(menu::SelectPrevious.boxed_clone(), cx); + } + }); + + workspace.register_action(|workspace, n: &Number, window, cx| { + let vim = workspace + .focused_pane(window, cx) + .read(cx) + .active_item() + .and_then(|item| item.act_as::(cx)) + .and_then(|editor| editor.read(cx).addon::().cloned()); + if let Some(vim) = vim { + let digit = n.0; + vim.entity.update(cx, |_, cx| { + cx.defer_in(window, move |vim, window, cx| { + vim.push_count_digit(digit, window, cx) + }) + }); + } else { + let count = Vim::globals(cx).pre_count.unwrap_or(0); + Vim::globals(cx).pre_count = Some( + count + .checked_mul(10) + .and_then(|c| c.checked_add(n.0)) + .unwrap_or(count), + ); + }; + }); + workspace.register_action(|_, _: &OpenDefaultKeymap, _, cx| { cx.emit(workspace::Event::OpenBundledFile { text: settings::vim_keymap(),