Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/core/src/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub mod action_id {
pub const TERMINAL_INCREASE_FONT: &str = "terminal.increase_font";
pub const TERMINAL_DECREASE_FONT: &str = "terminal.decrease_font";
pub const TERMINAL_RESET_FONT: &str = "terminal.reset_font";
pub const TERMINAL_SCROLL_LINE_UP: &str = "terminal.scroll_line_up";
pub const TERMINAL_SCROLL_LINE_DOWN: &str = "terminal.scroll_line_down";
pub const TERMINAL_SCROLL_PAGE_UP: &str = "terminal.scroll_page_up";
pub const TERMINAL_SCROLL_PAGE_DOWN: &str = "terminal.scroll_page_down";
pub const REMOTE_EDITOR_SEARCH: &str = "remote_editor.search";
pub const REMOTE_EDITOR_REPLACE: &str = "remote_editor.replace";
pub const REDIS_CLEAR_OUTPUT: &str = "redis.clear_output";
Expand Down
1 change: 1 addition & 0 deletions crates/terminal_view/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ mod render_layout;
mod render_surface;
mod resize_event_handler;
mod scroll;
mod scroll_keys;
mod selection_autoscroll;
mod selection_search;
mod session_log_config;
Expand Down
4 changes: 4 additions & 0 deletions crates/terminal_view/src/view/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ actions!(
IncreaseFont,
DecreaseFont,
ResetFont,
ScrollLineUp,
ScrollLineDown,
ScrollPageUp,
ScrollPageDown,
StartRecording,
PauseRecording,
ResumeRecording,
Expand Down
121 changes: 120 additions & 1 deletion crates/terminal_view/src/view/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ pub(super) const TERMINAL_SEARCH_BACKWARD_SHORTCUT: &str = "cmd-g";
#[cfg(not(target_os = "macos"))]
pub(super) const TERMINAL_SEARCH_BACKWARD_SHORTCUT: &str = "ctrl-shift-g";
pub(super) const TERMINAL_TOGGLE_VI_MODE_SHORTCUT: &str = "f7";
// 回滚历史滚动:沿用 Windows Terminal / Alacritty 的约定,翻页用 Shift+PageUp/PageDown,
// 逐行用 Ctrl+Shift+Up/Down。不占用 Shift+Up/Down,它们在历史提示中仍用于导航。
pub(super) const TERMINAL_SCROLL_LINE_UP_SHORTCUT: &str = "ctrl-shift-up";
pub(super) const TERMINAL_SCROLL_LINE_DOWN_SHORTCUT: &str = "ctrl-shift-down";
pub(super) const TERMINAL_SCROLL_PAGE_UP_SHORTCUT: &str = "shift-pageup";
pub(super) const TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT: &str = "shift-pagedown";
// Escape is terminal input (for example, Vim uses it to leave insert mode).
// ClearSelection remains available from the context menu and can still be
// assigned by users, but must not consume Escape by default.
Expand Down Expand Up @@ -154,6 +160,42 @@ pub(super) fn init_keybindings(cx: &App) -> Vec<KeyBinding> {
.into_iter()
.map(|key| KeyBinding::new(&key, ResetFont, Some(TERMINAL_CONTEXT))),
);
keybindings.extend(
shortcuts_for(
cx,
action_id::TERMINAL_SCROLL_LINE_UP,
&[TERMINAL_SCROLL_LINE_UP_SHORTCUT],
)
.into_iter()
.map(|key| KeyBinding::new(&key, ScrollLineUp, Some(TERMINAL_CONTEXT))),
);
keybindings.extend(
shortcuts_for(
cx,
action_id::TERMINAL_SCROLL_LINE_DOWN,
&[TERMINAL_SCROLL_LINE_DOWN_SHORTCUT],
)
.into_iter()
.map(|key| KeyBinding::new(&key, ScrollLineDown, Some(TERMINAL_CONTEXT))),
);
keybindings.extend(
shortcuts_for(
cx,
action_id::TERMINAL_SCROLL_PAGE_UP,
&[TERMINAL_SCROLL_PAGE_UP_SHORTCUT],
)
.into_iter()
.map(|key| KeyBinding::new(&key, ScrollPageUp, Some(TERMINAL_CONTEXT))),
);
keybindings.extend(
shortcuts_for(
cx,
action_id::TERMINAL_SCROLL_PAGE_DOWN,
&[TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT],
)
.into_iter()
.map(|key| KeyBinding::new(&key, ScrollPageDown, Some(TERMINAL_CONTEXT))),
);
keybindings
}

Expand Down Expand Up @@ -251,6 +293,34 @@ pub(super) fn refreshable_keybindings(cx: &App) -> Vec<KeyBinding> {
Some(TERMINAL_CONTEXT),
ResetFont,
));
keybindings.extend(rebind_keybindings(
cx,
action_id::TERMINAL_SCROLL_LINE_UP,
&[TERMINAL_SCROLL_LINE_UP_SHORTCUT],
Some(TERMINAL_CONTEXT),
ScrollLineUp,
));
keybindings.extend(rebind_keybindings(
cx,
action_id::TERMINAL_SCROLL_LINE_DOWN,
&[TERMINAL_SCROLL_LINE_DOWN_SHORTCUT],
Some(TERMINAL_CONTEXT),
ScrollLineDown,
));
keybindings.extend(rebind_keybindings(
cx,
action_id::TERMINAL_SCROLL_PAGE_UP,
&[TERMINAL_SCROLL_PAGE_UP_SHORTCUT],
Some(TERMINAL_CONTEXT),
ScrollPageUp,
));
keybindings.extend(rebind_keybindings(
cx,
action_id::TERMINAL_SCROLL_PAGE_DOWN,
&[TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT],
Some(TERMINAL_CONTEXT),
ScrollPageDown,
));
keybindings
}

Expand Down Expand Up @@ -320,6 +390,22 @@ pub(super) fn is_terminal_action_shortcut(keystroke: &Keystroke, cx: &App) -> bo
action_id::TERMINAL_RESET_FONT,
vec![terminal_platform_shortcut("cmd-0", "ctrl-0")],
),
(
action_id::TERMINAL_SCROLL_LINE_UP,
vec![TERMINAL_SCROLL_LINE_UP_SHORTCUT],
),
(
action_id::TERMINAL_SCROLL_LINE_DOWN,
vec![TERMINAL_SCROLL_LINE_DOWN_SHORTCUT],
),
(
action_id::TERMINAL_SCROLL_PAGE_UP,
vec![TERMINAL_SCROLL_PAGE_UP_SHORTCUT],
),
(
action_id::TERMINAL_SCROLL_PAGE_DOWN,
vec![TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT],
),
]
.into_iter()
.any(|(action_id, defaults)| {
Expand All @@ -329,10 +415,43 @@ pub(super) fn is_terminal_action_shortcut(keystroke: &Keystroke, cx: &App) -> bo

#[cfg(test)]
mod tests {
use super::TERMINAL_CLEAR_SELECTION_DEFAULTS;
use super::{
TERMINAL_CLEAR_SELECTION_DEFAULTS, TERMINAL_SCROLL_LINE_DOWN_SHORTCUT,
TERMINAL_SCROLL_LINE_UP_SHORTCUT, TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT,
TERMINAL_SCROLL_PAGE_UP_SHORTCUT,
};
use gpui::Keystroke;

#[test]
fn clear_selection_does_not_consume_escape_by_default() {
assert!(TERMINAL_CLEAR_SELECTION_DEFAULTS.is_empty());
}

#[test]
fn scroll_shortcuts_are_valid_keystrokes() {
for shortcut in [
TERMINAL_SCROLL_LINE_UP_SHORTCUT,
TERMINAL_SCROLL_LINE_DOWN_SHORTCUT,
TERMINAL_SCROLL_PAGE_UP_SHORTCUT,
TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT,
] {
assert!(Keystroke::parse(shortcut).is_ok(), "{shortcut} 必须可解析");
}
}

#[test]
fn scroll_shortcuts_do_not_shadow_plain_terminal_keys() {
// 无修饰的 PageUp/PageDown 与 Shift+Up/Down 属于 shell/TUI 输入,不能被占用。
for shortcut in [
TERMINAL_SCROLL_LINE_UP_SHORTCUT,
TERMINAL_SCROLL_LINE_DOWN_SHORTCUT,
TERMINAL_SCROLL_PAGE_UP_SHORTCUT,
TERMINAL_SCROLL_PAGE_DOWN_SHORTCUT,
] {
let modifiers = Keystroke::parse(shortcut).unwrap().modifiers;
assert!(modifiers.shift, "{shortcut} 必须带 Shift");
}
assert_ne!("shift-up", TERMINAL_SCROLL_LINE_UP_SHORTCUT);
assert_ne!("shift-down", TERMINAL_SCROLL_LINE_DOWN_SHORTCUT);
}
}
4 changes: 4 additions & 0 deletions crates/terminal_view/src/view/render_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ impl TerminalView {
.on_action(cx.listener(Self::increase_font))
.on_action(cx.listener(Self::decrease_font))
.on_action(cx.listener(Self::reset_font))
.on_action(cx.listener(Self::scroll_line_up))
.on_action(cx.listener(Self::scroll_line_down))
.on_action(cx.listener(Self::scroll_page_up))
.on_action(cx.listener(Self::scroll_page_down))
.on_key_down(cx.listener(Self::handle_key_event))
.flex_1()
.relative()
Expand Down
2 changes: 1 addition & 1 deletion crates/terminal_view/src/view/scroll.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::*;

impl TerminalView {
fn try_scroll_vi_mode(&self, lines: i32, cx: &Context<Self>) -> bool {
pub(super) fn try_scroll_vi_mode(&self, lines: i32, cx: &Context<Self>) -> bool {
let term = self.terminal.read(cx).term().clone();
let Some(mut term) = term.try_lock_unfair() else {
return false;
Expand Down
84 changes: 84 additions & 0 deletions crates/terminal_view/src/view/scroll_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use super::*;

/// 翻页步长:保留一行上下文,与 xterm / Windows Terminal 的 Shift+PageUp 一致。
pub(super) fn terminal_page_scroll_lines(screen_lines: usize) -> i32 {
screen_lines.saturating_sub(1).max(1) as i32
}

impl TerminalView {
pub(super) fn scroll_line_up(
&mut self,
_: &ScrollLineUp,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.scroll_history_by(1, cx);
}

pub(super) fn scroll_line_down(
&mut self,
_: &ScrollLineDown,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.scroll_history_by(-1, cx);
}

pub(super) fn scroll_page_up(
&mut self,
_: &ScrollPageUp,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let page = terminal_page_scroll_lines(self.terminal_frame_snapshot.screen_lines);
self.scroll_history_by(page, cx);
}

pub(super) fn scroll_page_down(
&mut self,
_: &ScrollPageDown,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let page = terminal_page_scroll_lines(self.terminal_frame_snapshot.screen_lines);
self.scroll_history_by(-page, cx);
}

/// 按行数滚动回滚历史,正数向历史方向(上)。语义与鼠标滚轮一致:
/// VI 模式移动 vi 光标,其余模式移动显示偏移。
fn scroll_history_by(&mut self, lines: i32, cx: &mut Context<Self>) {
let accepts_live_input = self.accepts_live_terminal_input(cx);
let mode = self.terminal_frame_snapshot.mode;
if accepts_live_input && mode.contains(TermMode::ALT_SCREEN) {
// alt-screen 没有回滚历史,按键应继续交给 vim/less 等 TUI 程序。
cx.propagate();
return;
}

self.dismiss_history_prompt();
if accepts_live_input && mode.contains(TermMode::VI) {
if self.pending_vi_scroll_lines != 0 {
self.pending_vi_scroll_lines = self.pending_vi_scroll_lines.saturating_add(lines);
self.schedule_terminal_render_retry(cx);
return;
}
if !self.try_scroll_vi_mode(lines, cx) {
self.pending_vi_scroll_lines = lines;
self.schedule_terminal_render_retry(cx);
return;
}
} else if !self.scrollbar_handle.try_scroll_display_delta(lines) {
let base = self
.scrollbar_handle
.future_display_offset()
.unwrap_or(self.terminal_frame_snapshot.display_offset);
let target = (base as i64 + lines as i64)
.clamp(0, self.terminal_frame_snapshot.history_size as i64)
as usize;
self.scrollbar_handle.put_back_future_display_offset(target);
self.schedule_terminal_render_retry(cx);
return;
}
cx.notify();
}
}
1 change: 1 addition & 0 deletions crates/terminal_view/src/view/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ mod history_interaction;
mod layout;
mod mouse;
mod paste;
mod scroll_keys;
4 changes: 4 additions & 0 deletions crates/terminal_view/src/view/tests/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ fn quick_command_shortcuts_do_not_override_any_terminal_action_shortcut() {
"action_id::TERMINAL_INCREASE_FONT",
"action_id::TERMINAL_DECREASE_FONT",
"action_id::TERMINAL_RESET_FONT",
"action_id::TERMINAL_SCROLL_LINE_UP",
"action_id::TERMINAL_SCROLL_LINE_DOWN",
"action_id::TERMINAL_SCROLL_PAGE_UP",
"action_id::TERMINAL_SCROLL_PAGE_DOWN",
] {
assert!(
guard.contains(action_id),
Expand Down
52 changes: 52 additions & 0 deletions crates/terminal_view/src/view/tests/scroll_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use super::super::scroll_keys::terminal_page_scroll_lines;

#[test]
fn page_scroll_keeps_one_line_of_context() {
assert_eq!(23, terminal_page_scroll_lines(24));
assert_eq!(1, terminal_page_scroll_lines(2));
}

#[test]
fn page_scroll_never_degenerates_to_zero() {
assert_eq!(1, terminal_page_scroll_lines(1));
assert_eq!(1, terminal_page_scroll_lines(0));
}

#[test]
fn scroll_actions_are_registered_and_bound_in_terminal_context() {
let surface = include_str!("../render_surface.rs");
let bindings = include_str!("../keybindings.rs");

for (handler, action) in [
("Self::scroll_line_up", "ScrollLineUp"),
("Self::scroll_line_down", "ScrollLineDown"),
("Self::scroll_page_up", "ScrollPageUp"),
("Self::scroll_page_down", "ScrollPageDown"),
] {
assert!(surface.contains(handler), "{handler} 必须注册为 action");
assert!(
bindings.matches(&format!("{action},")).count() >= 2,
"{action} 必须同时出现在初始绑定与可刷新绑定中"
);
}
}

#[test]
fn scroll_keys_yield_to_alt_screen_programs() {
let source = include_str!("../scroll_keys.rs");
let body = source
.split("fn scroll_history_by")
.nth(1)
.expect("scroll_history_by should exist");
let alt_screen = body
.find("mode.contains(TermMode::ALT_SCREEN)")
.expect("alt-screen 必须被识别");
let propagate = body
.find("cx.propagate()")
.expect("alt-screen 必须放行按键");
let scroll = body
.find("try_scroll_display_delta")
.expect("普通屏幕应滚动历史");

assert!(alt_screen < propagate && propagate < scroll);
}
16 changes: 16 additions & 0 deletions main/locales/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4101,6 +4101,22 @@ Settings:
en: Toggle Vi Mode
zh-CN: 切换 Vi 模式
zh-HK: 切換 Vi 模式
terminal_scroll_line_up:
en: Scroll Up One Line
zh-CN: 向上滚动一行
zh-HK: 向上捲動一行
terminal_scroll_line_down:
en: Scroll Down One Line
zh-CN: 向下滚动一行
zh-HK: 向下捲動一行
terminal_scroll_page_up:
en: Scroll Up One Page
zh-CN: 向上滚动一屏
zh-HK: 向上捲動一頁
terminal_scroll_page_down:
en: Scroll Down One Page
zh-CN: 向下滚动一屏
zh-HK: 向下捲動一頁
database_focus_search:
en: Focus Search
zh-CN: 聚焦搜索
Expand Down
Loading
Loading