project_panel vim counts and shortcuts

This commit is contained in:
AidanV 2025-05-25 23:47:08 -07:00
parent c731bb6d91
commit d08b72b872
6 changed files with 169 additions and 17 deletions

1
Cargo.lock generated
View file

@ -17937,6 +17937,7 @@ dependencies = [
"language",
"log",
"lsp",
"menu",
"multi_buffer",
"nvim-rs",
"parking_lot",

View file

@ -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"
}
},
{

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);
}
}
}

View file

@ -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<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
@ -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<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()
@ -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<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,

View file

@ -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

View file

@ -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::<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(),