diff --git a/Cargo.lock b/Cargo.lock index a8d1a929519c917634fd22e5bc639e8f45398558..845e4a3eb288645a1bd7d911446ebaaa4c4d5abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17707,6 +17707,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 800cb52bdad65c4276ec9e854828d10c112e4bc2..ae6bc8391e30e1bc61622e23e9f3aad773f4f1a7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -884,10 +884,12 @@ "/": "project_panel::NewSearchInDirectory", "d": "project_panel::NewDirectory", "enter": "project_panel::OpenPermanent", - "escape": "project_panel::ToggleFocus", + "escape": "vim::ToggleProjectPanelFocus", "h": "project_panel::CollapseSelectedEntry", - "j": "menu::SelectNext", - "k": "menu::SelectPrevious", + "j": "vim::MenuSelectNext", + "k": "vim::MenuSelectPrevious", + "down": "vim::MenuSelectNext", + "up": "vim::MenuSelectPrevious", "l": "project_panel::ExpandSelectedEntry", "shift-d": "project_panel::Delete", "shift-r": "project_panel::Rename", @@ -906,7 +908,22 @@ "{": "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", + "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] } }, { diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 9c601aac1d72d915b2e8c4b3866de88b966c05f0..b265fb390e9083acc9139e243c82d2c15bc83224 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 7017fec7865dcc98fde295ee5838c1e81c6b511d..f5b1e2d255bf17f5513c9995ff54bbc53507b122 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -85,6 +85,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 @@ -278,6 +279,16 @@ actions!( UnfoldDirectory, /// Folds the selected directory. FoldDirectory, + /// Scroll half a page upwards + ScrollUp, + /// Scroll half a page downwards + ScrollDown, + /// Scroll until the cursor displays at the center + ScrollCursorCenter, + /// Scroll until the cursor displays at the top + ScrollCursorTop, + /// Scroll until the cursor displays at the bottom + ScrollCursorBottom, /// Selects the parent directory. SelectParent, /// Selects the next entry with git changes. @@ -634,6 +645,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, @@ -2080,6 +2092,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() @@ -5198,6 +5256,11 @@ impl Render for ProjectPanel { }, )) .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)) @@ -5272,7 +5335,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 41780dd3487a3f04229f48d1002c72c1dec0e044..ad84eecd91ddfc4b300b437936aba0ac21b4e41c 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -34,6 +34,7 @@ multi_buffer.workspace = true nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true } picker.workspace = true project.workspace = true +project_panel.workspace = true regex.workspace = true schemars.workspace = true search.workspace = true @@ -43,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 3e09108aee2058685969a04e430a0dcd7a32edbb..d503f39f6c6be5881e5392df766dc40f5e1ec4ff 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -240,6 +240,12 @@ actions!( PushReplaceWithRegister, /// Toggles comments. PushToggleComments, + /// Selects (count) next menu item + MenuSelectNext, + /// Selects (count) previous menu item + MenuSelectPrevious, + /// Clears count or toggles project panel focus + ToggleProjectPanelFocus, /// Starts a match operation. PushHelixMatch, ] @@ -271,6 +277,53 @@ 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(|_, _: &ToggleProjectPanelFocus, window, cx| { + if Vim::take_count(cx).is_none() { + window.dispatch_action(project_panel::ToggleFocus.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(),