This commit is contained in:
吴杨帆 2026-05-17 17:42:11 +08:00 committed by GitHub
commit 712055650c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 158 additions and 6 deletions

View file

@ -870,9 +870,14 @@ impl Component for DiffComponent {
Ok(EventState::Consumed)
} else if key_match(e, self.key_config.keys.move_left)
{
self.horizontal_scroll
.move_right(HorizontalScrollType::Left);
Ok(EventState::Consumed)
if self
.horizontal_scroll
.move_right(HorizontalScrollType::Left)
{
Ok(EventState::Consumed)
} else {
Ok(EventState::NotConsumed)
}
} else if key_match(
e,
self.key_config.keys.diff_hunk_next,

View file

@ -0,0 +1,90 @@
//! Debounce arrow-key edge navigation against key autorepeat.
use super::key_list::GituiKeyEvent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::{
collections::HashMap,
time::{Duration, Instant},
};
const DEFAULT_COOLDOWN: Duration = Duration::from_millis(500);
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
struct KeyId {
code: KeyCode,
modifiers: KeyModifiers,
}
impl From<GituiKeyEvent> for KeyId {
fn from(key: GituiKeyEvent) -> Self {
Self {
code: key.code,
modifiers: key.modifiers,
}
}
}
///
pub struct KeyRepeatGuard {
last: HashMap<KeyId, Instant>,
cooldown: Duration,
}
impl KeyRepeatGuard {
///
pub fn new() -> Self {
Self {
last: HashMap::new(),
cooldown: DEFAULT_COOLDOWN,
}
}
#[cfg(test)]
pub fn with_cooldown(cooldown: Duration) -> Self {
Self {
last: HashMap::new(),
cooldown,
}
}
///
pub fn record(&mut self, key: GituiKeyEvent) {
self.last.insert(key.into(), Instant::now());
}
///
pub fn record_key_event(&mut self, key: &KeyEvent) {
self.record(GituiKeyEvent {
code: key.code,
modifiers: key.modifiers,
});
}
/// Whether edge navigation (leaving a scrollable view) should run now.
pub fn allow_edge_navigation(&self, key: GituiKeyEvent) -> bool {
self.last
.get(&key.into())
.is_none_or(|t| Instant::now().duration_since(*t) >= self.cooldown)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
use std::thread::sleep;
#[test]
fn test_blocks_rapid_repeats() {
let mut guard = KeyRepeatGuard::with_cooldown(Duration::from_millis(50));
let key =
GituiKeyEvent::new(KeyCode::Up, KeyModifiers::empty());
assert!(guard.allow_edge_navigation(key));
guard.record(key);
assert!(!guard.allow_edge_navigation(key));
sleep(Duration::from_millis(60));
assert!(guard.allow_edge_navigation(key));
}
}

View file

@ -1,6 +1,8 @@
mod key_config;
mod key_list;
mod key_repeat_guard;
mod symbols;
pub use key_config::{KeyConfig, SharedKeyConfig};
pub use key_list::key_match;
pub use key_repeat_guard::KeyRepeatGuard;

View file

@ -7,7 +7,7 @@ use crate::{
DiffComponent, DrawableComponent, EventState,
FileTreeItemKind,
},
keys::{key_match, SharedKeyConfig},
keys::{key_match, KeyRepeatGuard, SharedKeyConfig},
options::SharedOptions,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
strings, try_or_popup,
@ -81,6 +81,7 @@ pub struct Status {
git_action_executed: bool,
options: SharedOptions,
key_config: SharedKeyConfig,
key_repeat_guard: KeyRepeatGuard,
}
impl DrawableComponent for Status {
@ -198,6 +199,7 @@ impl Status {
env.repo.clone(),
),
key_config: env.key_config.clone(),
key_repeat_guard: KeyRepeatGuard::new(),
options: env.options.clone(),
repo: env.repo.clone(),
}
@ -581,6 +583,17 @@ impl Status {
}
}
fn record_navigation_key(&mut self, key: &crossterm::event::KeyEvent) {
let keys = &self.key_config.keys;
if key_match(key, keys.move_up)
|| key_match(key, keys.move_down)
|| key_match(key, keys.move_left)
|| key_match(key, keys.move_right)
{
self.key_repeat_guard.record_key_event(key);
}
}
fn fetch(&self) {
if self.can_fetch() {
self.queue.push(InternalEvent::FetchRemotes);
@ -820,6 +833,9 @@ impl Component for Status {
if event_pump(ev, self.components_mut().as_mut_slice())?
.is_consumed()
{
if let Event::Key(k) = ev {
self.record_navigation_key(k);
}
self.git_action_executed = true;
return Ok(EventState::Consumed);
}
@ -845,6 +861,25 @@ impl Component for Status {
) && self.can_focus_diff()
{
self.switch_focus(Focus::Diff).map(Into::into)
} else if key_match(
k,
self.key_config.keys.move_left,
) && self.is_focus_on_diff()
{
let binding = self.key_config.keys.move_left;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(match self.diff_target {
DiffTarget::Stage => Focus::Stage,
DiffTarget::WorkingDir => Focus::WorkDir,
})
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(
k,
self.key_config.keys.exit_popup,
@ -858,12 +893,32 @@ impl Component for Status {
&& self.focus == Focus::WorkDir
&& !self.index.is_empty()
{
self.switch_focus(Focus::Stage).map(Into::into)
let binding = self.key_config.keys.move_down;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(Focus::Stage)
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(k, self.key_config.keys.move_up)
&& self.focus == Focus::Stage
&& !self.index_wd.is_empty()
{
self.switch_focus(Focus::WorkDir).map(Into::into)
let binding = self.key_config.keys.move_up;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(Focus::WorkDir)
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(
k,
self.key_config.keys.select_branch,