Cargo.lock 🔗
@@ -17707,6 +17707,7 @@ dependencies = [
"language",
"log",
"lsp",
+ "menu",
"multi_buffer",
"nvim-rs",
"parking_lot",
AidanV created
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
Cargo.lock | 1
assets/keymaps/vim.json | 25 +++++++-
crates/gpui/src/elements/uniform_list.rs | 54 +++++++++++++++----
crates/project_panel/src/project_panel.rs | 66 ++++++++++++++++++++++++
crates/vim/Cargo.toml | 2
crates/vim/src/vim.rs | 53 ++++++++++++++++++++
6 files changed, 183 insertions(+), 18 deletions(-)
@@ -17707,6 +17707,7 @@ dependencies = [
"language",
"log",
"lsp",
+ "menu",
"multi_buffer",
"nvim-rs",
"parking_lot",
@@ -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]
}
},
{
@@ -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);
}
}
}
@@ -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,
@@ -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
@@ -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(),