editor: Context menu aside scrolling (#35985)
Add support for scrolling the contents rendered aside an `editor::code_context_menus::CodeContextMenu` by introducing the `scroll_aside` method. For now this method is only implemented for the `CodeContextMenu::Completions` variant, which will scroll the aside contents for an `editor::code_context_menus::CompletionsMenu` element, as a `ScrollHandle` is added to the aside content that is rendered. In order to be possible to trigger this via keybindings, a new editor action is introduced, `ContextMenuScrollAside`, which accepts a number of lines or pages to scroll the content by. Lastly, the default keymaps for both MacOS and Linux, as well as for Zed's vim mode, are updated to ensure that the following keybindings are supported when a completion menu is open and the completion item's documentation is rendered aside: - `ctrl-e` - `ctrl-y` - `ctrl-d` - `ctrl-u` ### Recording https://github.com/user-attachments/assets/02043763-87ea-46f5-9768-00e907127b69 --- Closes #13194 Release Notes: - Added support for scrolling the documentation panel shown alongside the completion menu in the editor with `cltr-d`, `ctrl-u`, `ctrl-e` and `ctrl-y` --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
co-authored by
Conrad Irwin
MrSubidubi
parent
fcc3d1092f
commit
e9252a7a74
+10
-1
@@ -324,7 +324,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert",
|
||||
"context": "vim_mode == insert && !menu",
|
||||
"bindings": {
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
@@ -354,6 +354,15 @@
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-d": "vim::ScrollDown",
|
||||
"ctrl-u": "vim::ScrollUp",
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
||||
"bindings": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::scroll::ScrollAmount;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
|
||||
Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
|
||||
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
|
||||
SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
|
||||
uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::CodeLabel;
|
||||
@@ -184,6 +186,20 @@ impl CodeContextMenu {
|
||||
CodeContextMenu::CodeActions(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_aside(
|
||||
&mut self,
|
||||
scroll_amount: ScrollAmount,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
match self {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
completions_menu.scroll_aside(scroll_amount, window, cx)
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuOrigin {
|
||||
@@ -207,6 +223,9 @@ pub struct CompletionsMenu {
|
||||
filter_task: Task<()>,
|
||||
cancel_filter: Arc<AtomicBool>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
// The `ScrollHandle` used on the Markdown documentation rendered on the
|
||||
// side of the completions menu.
|
||||
pub scroll_handle_aside: ScrollHandle,
|
||||
resolve_completions: bool,
|
||||
show_completion_documentation: bool,
|
||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||
@@ -279,6 +298,7 @@ impl CompletionsMenu {
|
||||
filter_task: Task::ready(()),
|
||||
cancel_filter: Arc::new(AtomicBool::new(false)),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
scroll_handle_aside: ScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
markdown_cache: RefCell::new(VecDeque::new()).into(),
|
||||
@@ -348,6 +368,7 @@ impl CompletionsMenu {
|
||||
filter_task: Task::ready(()),
|
||||
cancel_filter: Arc::new(AtomicBool::new(false)),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
scroll_handle_aside: ScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
@@ -911,6 +932,7 @@ impl CompletionsMenu {
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle_aside)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
@@ -1175,6 +1197,23 @@ impl CompletionsMenu {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn scroll_aside(
|
||||
&mut self,
|
||||
amount: ScrollAmount,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let mut offset = self.scroll_handle_aside.offset();
|
||||
|
||||
offset.y -= amount.pixels(
|
||||
window.line_height(),
|
||||
self.scroll_handle_aside.bounds().size.height - px(16.),
|
||||
) / 2.0;
|
||||
|
||||
cx.notify();
|
||||
self.scroll_handle_aside.set_offset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -188,22 +188,26 @@ impl Editor {
|
||||
|
||||
pub fn scroll_hover(
|
||||
&mut self,
|
||||
amount: &ScrollAmount,
|
||||
amount: ScrollAmount,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let selection = self.selections.newest_anchor().head();
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
|
||||
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
|
||||
if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
|
||||
popover
|
||||
.symbol_range
|
||||
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
popover.scroll(amount, window, cx);
|
||||
true
|
||||
}) {
|
||||
popover.scroll(amount, window, cx);
|
||||
true
|
||||
} else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
||||
context_menu.scroll_aside(amount, window, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_click_reveal_task(
|
||||
|
||||
@@ -896,7 +896,7 @@ impl InfoPopover {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
let mut current = self.scroll_handle.offset();
|
||||
current.y -= amount.pixels(
|
||||
window.line_height(),
|
||||
|
||||
@@ -15,7 +15,7 @@ impl ScrollDirection {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
|
||||
pub enum ScrollAmount {
|
||||
// Scroll N lines (positive is towards the end of the document)
|
||||
Line(f32),
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Vim {
|
||||
Vim::take_forced_motion(cx);
|
||||
self.exit_temporary_normal(window, cx);
|
||||
self.update_editor(cx, |_, editor, cx| {
|
||||
scroll_editor(editor, move_cursor, &amount, window, cx)
|
||||
scroll_editor(editor, move_cursor, amount, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ impl Vim {
|
||||
fn scroll_editor(
|
||||
editor: &mut Editor,
|
||||
preserve_cursor_position: bool,
|
||||
amount: &ScrollAmount,
|
||||
amount: ScrollAmount,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -126,7 +126,7 @@ fn scroll_editor(
|
||||
ScrollAmount::Line(amount.lines(visible_line_count) - 1.0)
|
||||
}
|
||||
}
|
||||
_ => amount.clone(),
|
||||
_ => amount,
|
||||
};
|
||||
|
||||
editor.scroll_screen(&amount, window, cx);
|
||||
|
||||
+85
-2
@@ -8,13 +8,15 @@ use collections::HashMap;
|
||||
use command_palette::CommandPalette;
|
||||
use editor::{
|
||||
AnchorRangeExt, DisplayPoint, Editor, EditorMode, MultiBuffer, actions::DeleteLine,
|
||||
display_map::DisplayRow, test::editor_test_context::EditorTestContext,
|
||||
code_context_menus::CodeContextMenu, display_map::DisplayRow,
|
||||
test::editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext};
|
||||
use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext, px};
|
||||
use language::Point;
|
||||
pub use neovim_backed_test_context::*;
|
||||
use settings::SettingsStore;
|
||||
use ui::Pixels;
|
||||
use util::test::marked_text_ranges;
|
||||
pub use vim_test_context::*;
|
||||
|
||||
@@ -971,6 +973,87 @@ async fn test_comma_w(cx: &mut gpui::TestAppContext) {
|
||||
.assert_eq("hellˇo hello\nhello hello");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_menu_scroll_aside(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new_typescript(cx).await;
|
||||
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "Test Item".to_string(),
|
||||
documentation: Some(lsp::Documentation::String(
|
||||
"This is some very long documentation content that will be displayed in the aside panel for scrolling.\n".repeat(50)
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx.set_state("variableˇ", Mode::Insert);
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let mut initial_offset: Pixels = px(0.0);
|
||||
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let binding = editor.context_menu().borrow();
|
||||
let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else {
|
||||
panic!("Should have completions menu open");
|
||||
};
|
||||
|
||||
initial_offset = menu.scroll_handle_aside.offset().y;
|
||||
});
|
||||
|
||||
// The `ctrl-e` shortcut should scroll the completion menu's aside content
|
||||
// down, so the updated offset should be lower than the initial offset.
|
||||
cx.simulate_keystroke("ctrl-e");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let binding = editor.context_menu().borrow();
|
||||
let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else {
|
||||
panic!("Should have completions menu open");
|
||||
};
|
||||
|
||||
assert!(menu.scroll_handle_aside.offset().y < initial_offset);
|
||||
});
|
||||
|
||||
// The `ctrl-y` shortcut should do the inverse scrolling as `ctrl-e`, so the
|
||||
// offset should now be the same as the initial offset.
|
||||
cx.simulate_keystroke("ctrl-y");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let binding = editor.context_menu().borrow();
|
||||
let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else {
|
||||
panic!("Should have completions menu open");
|
||||
};
|
||||
|
||||
assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset);
|
||||
});
|
||||
|
||||
// The `ctrl-d` shortcut should scroll the completion menu's aside content
|
||||
// down, so the updated offset should be lower than the initial offset.
|
||||
cx.simulate_keystroke("ctrl-d");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let binding = editor.context_menu().borrow();
|
||||
let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else {
|
||||
panic!("Should have completions menu open");
|
||||
};
|
||||
|
||||
assert!(menu.scroll_handle_aside.offset().y < initial_offset);
|
||||
});
|
||||
|
||||
// The `ctrl-u` shortcut should do the inverse scrolling as `ctrl-u`, so the
|
||||
// offset should now be the same as the initial offset.
|
||||
cx.simulate_keystroke("ctrl-u");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let binding = editor.context_menu().borrow();
|
||||
let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else {
|
||||
panic!("Should have completions menu open");
|
||||
};
|
||||
|
||||
assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new_typescript(cx).await;
|
||||
|
||||
@@ -49,6 +49,10 @@ impl VimTestContext {
|
||||
Self::new_with_lsp(
|
||||
EditorLspTestContext::new_typescript(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
|
||||
prepare_provider: Some(true),
|
||||
work_done_progress_options: Default::default(),
|
||||
|
||||
Reference in New Issue
Block a user