vim: Add vim counts and vim shortcuts to project_panel (#36653)

Closes #10930 
Closes #11353

Release Notes:

- Adds commands to project_panel
  - `ctrl-u` scrolls the project_panel up half of the visible entries
  - `ctrl-d` scrolls the project_panel down half of the visible entries
  - `z z` scrolls current selection to center of window
  - `z t`  scrolls current selection to top of window
  - `z b` scrolls current selection to bottom of window
  - `{num} j` and `{num} k` now move up and  down with a count
This commit is contained in:
AidanV
2025-09-29 15:53:59 -06:00
committed by GitHub
parent fbb4dcf2b1
commit 9c60bc3837
6 changed files with 183 additions and 18 deletions
Generated
+1
View File
@@ -17707,6 +17707,7 @@ dependencies = [
"language",
"log",
"lsp",
"menu",
"multi_buffer",
"nvim-rs",
"parking_lot",
+21 -4
View File
@@ -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]
}
},
{
+41 -13
View File
@@ -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);
}
}
}
+65 -1
View File
@@ -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<Task<()>>,
rendered_entries_len: usize,
visible_entries: Vec<VisibleEntriesForWorktree>,
/// 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<Self>) {
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<Self>) {
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<Self>,
) {
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<Self>) {
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<Self>,
) {
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<Self>) {
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<usize>, 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,
+2
View File
@@ -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
+53
View File
@@ -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::<Editor>(cx))
.and_then(|editor| editor.read(cx).addon::<VimAddon>().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(),