which_key: Improve pending keystrokes indicator to include untimed chords (#64362)
Closes #64338 The pending keystrokes indicator now shows chords without a timeout, with no countdown circle. For example, pressing `cmd-k` shows that Zed is waiting for another key. Few cases which needed special handling for untimed chords case: - Hovering already pauses a timed chord, and GPUI preserves that pause when the chord advances. An untimed chord has no paused timeout to carry forward. We pause its newly created timeout if the pointer is still over the indicator or popover. - For timed chords, the timeout already stays paused during the popover's 300 ms dismissal delay. If an untimed chord gains a timeout during that delay, it now follows the same rule. The popover still closes at the original deadline, then the timeout resumes with a full second remaining. <img width="308" height="182" alt="CleanShot 2026-09-17 at 11 45 33 AM@2x" src="https://github.com/user-attachments/assets/9baa5fde-de9b-447d-9b02-b712a3065685" /> Release Notes: - Improved the pending keystrokes indicator to show chords without a timeout, such as shortcuts starting with `cmd-k`.
This commit is contained in:
1 parent
20d55798db
commit
890edba9ce
7 files changed
+297
-63
No files matched your search
@@ -1973,9 +1973,9 @@
|
||||
"line_endings_button": false,
|
||||
// Control when to show the active encoding in the status bar.
|
||||
"active_encoding_button": "non_utf8",
|
||||
// Whether to show an indicator with a countdown while timed multi-stroke
|
||||
// input is pending. Hovering the indicator pauses the timeout. Its binding
|
||||
// preview popover is disabled when "which_key.enabled" is true.
|
||||
// Whether to show an indicator while multi-stroke input is pending.
|
||||
// If the input has a timeout, a countdown is shown and hovering pauses it.
|
||||
// Its binding preview popover is disabled when "which_key.enabled" is true.
|
||||
"pending_keystrokes_indicator": true,
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
|
||||
@@ -581,8 +581,8 @@ pub struct StatusBarSettingsContent {
|
||||
///
|
||||
/// Default: non_utf8
|
||||
pub active_encoding_button: Option<EncodingDisplayOptions>,
|
||||
/// Whether to show an indicator with a countdown while timed multi-stroke input is pending.
|
||||
/// Hovering the indicator pauses the timeout.
|
||||
/// Whether to show an indicator while multi-stroke input is pending.
|
||||
/// If the input has a timeout, a countdown is shown and hovering the indicator pauses it.
|
||||
/// Its binding preview popover is disabled when the which-key popup is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -4177,7 +4177,7 @@ fn window_and_layout_page() -> SettingsPage {
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Pending Keystrokes Indicator",
|
||||
description: "Show an indicator with a countdown while a multi-stroke key binding is pending. Its binding preview popover is disabled when the which-key menu is enabled.",
|
||||
description: "Show an indicator while a multi-stroke key binding is pending. If the input has a timeout, a countdown is shown and hovering pauses it. Its binding preview popover is disabled when the which-key menu is enabled.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("status_bar.pending_keystrokes_indicator"),
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
const MAX_TOOLTIP_BINDINGS: usize = 10;
|
||||
const POPOVER_HIDE_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
/// A status bar item shown while timed pending input can complete a multi-stroke key binding.
|
||||
/// A status bar item shown while pending input can complete a multi-stroke key binding.
|
||||
pub struct PendingKeystrokesIndicator {
|
||||
render_state: Option<Rc<IndicatorRenderState>>,
|
||||
pending_input_generation: u64,
|
||||
@@ -46,6 +46,10 @@ struct IndicatorRenderState {
|
||||
keystrokes: Rc<[KeybindingKeystroke]>,
|
||||
pending_input_generation: u64,
|
||||
bindings: Vec<(Rc<[KeybindingKeystroke]>, SharedString)>,
|
||||
timeout: Option<IndicatorTimeout>,
|
||||
}
|
||||
|
||||
struct IndicatorTimeout {
|
||||
timeout_duration: Duration,
|
||||
remaining_duration: Duration,
|
||||
timeout_paused: bool,
|
||||
@@ -58,6 +62,7 @@ impl PendingKeystrokesIndicator {
|
||||
if this.refresh_render_state(window, cx) {
|
||||
cx.notify();
|
||||
}
|
||||
this.update_pointer_over_state(window, cx);
|
||||
});
|
||||
|
||||
let mut enabled = Self::enabled(cx);
|
||||
@@ -112,9 +117,6 @@ impl PendingKeystrokesIndicator {
|
||||
let Some(pending_input) = window.pending_input() else {
|
||||
return self.clear_render_state(window, cx);
|
||||
};
|
||||
let Some(timeout) = pending_input.timeout() else {
|
||||
return self.clear_render_state(window, cx);
|
||||
};
|
||||
let keystrokes = pending_input.keystrokes();
|
||||
|
||||
let mut bindings = bindings_for_pending_input(window, keystrokes)
|
||||
@@ -140,17 +142,32 @@ impl PendingKeystrokesIndicator {
|
||||
text_a == text_b && action_a == action_b
|
||||
});
|
||||
|
||||
let keystrokes = map_pending_keystrokes(keystrokes, cx.keyboard_mapper().as_ref());
|
||||
// Pausing or resuming the timer also notifies observers.
|
||||
// Only a change in pending keys should close the popover early.
|
||||
if self.popover.visible
|
||||
&& !self.popover.is_pointer_over()
|
||||
&& self
|
||||
.render_state
|
||||
.as_ref()
|
||||
.is_some_and(|previous| previous.keystrokes.as_ref() != keystrokes.as_slice())
|
||||
{
|
||||
self.popover = PopoverState::default();
|
||||
}
|
||||
|
||||
self.pending_input_generation = self.pending_input_generation.wrapping_add(1);
|
||||
self.render_state = Some(Rc::new(IndicatorRenderState {
|
||||
keystrokes: map_pending_keystrokes(keystrokes, cx.keyboard_mapper().as_ref()).into(),
|
||||
keystrokes: keystrokes.into(),
|
||||
pending_input_generation: self.pending_input_generation,
|
||||
bindings: bindings
|
||||
.into_iter()
|
||||
.map(|(_, keystrokes, action)| (Rc::from(keystrokes), action))
|
||||
.collect(),
|
||||
timeout_duration: timeout.duration(),
|
||||
remaining_duration: timeout.remaining(cx),
|
||||
timeout_paused: timeout.is_paused(),
|
||||
timeout: pending_input.timeout().map(|timeout| IndicatorTimeout {
|
||||
timeout_duration: timeout.duration(),
|
||||
remaining_duration: timeout.remaining(cx),
|
||||
timeout_paused: timeout.is_paused(),
|
||||
}),
|
||||
}));
|
||||
true
|
||||
}
|
||||
@@ -225,48 +242,50 @@ impl Render for PendingKeystrokesIndicator {
|
||||
let Some(render_state) = self.render_state().cloned() else {
|
||||
return div().hidden().into_any_element();
|
||||
};
|
||||
let remaining_fraction = if render_state.timeout_duration.is_zero() {
|
||||
0.0
|
||||
} else {
|
||||
(render_state.remaining_duration.as_secs_f32()
|
||||
/ render_state.timeout_duration.as_secs_f32())
|
||||
.clamp(0.0, 1.0)
|
||||
};
|
||||
|
||||
let button = ButtonLike::new("pending-keystrokes-indicator")
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::dev::OpenKeyContextView.boxed_clone(), cx);
|
||||
})
|
||||
.child(if cx.reduce_motion() {
|
||||
Icon::new(IconName::CountdownTimer)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let progress = CircularProgress::new(
|
||||
remaining_fraction,
|
||||
1.0,
|
||||
rems_from_px(13_f32).to_pixels(window.rem_size()),
|
||||
cx,
|
||||
)
|
||||
.stroke_width(rems_from_px(2_f32).to_pixels(window.rem_size()))
|
||||
.progress_color(cx.theme().colors().text_muted);
|
||||
if render_state.timeout_paused || render_state.remaining_duration.is_zero() {
|
||||
progress.into_any_element()
|
||||
.when_some(render_state.timeout.as_ref(), |button, timeout| {
|
||||
let remaining_fraction = if timeout.timeout_duration.is_zero() {
|
||||
0.0
|
||||
} else {
|
||||
progress
|
||||
.with_animation(
|
||||
(
|
||||
"pending-keystrokes-countdown",
|
||||
render_state.pending_input_generation,
|
||||
),
|
||||
Animation::new(render_state.remaining_duration).with_max_fps(30.0),
|
||||
move |progress, delta| {
|
||||
progress.value(remaining_fraction * (1.0 - delta))
|
||||
},
|
||||
)
|
||||
(timeout.remaining_duration.as_secs_f32()
|
||||
/ timeout.timeout_duration.as_secs_f32())
|
||||
.clamp(0.0, 1.0)
|
||||
};
|
||||
button.child(if cx.reduce_motion() {
|
||||
Icon::new(IconName::CountdownTimer)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
}
|
||||
} else {
|
||||
let progress = CircularProgress::new(
|
||||
remaining_fraction,
|
||||
1.0,
|
||||
rems_from_px(13_f32).to_pixels(window.rem_size()),
|
||||
cx,
|
||||
)
|
||||
.stroke_width(rems_from_px(2_f32).to_pixels(window.rem_size()))
|
||||
.progress_color(cx.theme().colors().text_muted);
|
||||
if timeout.timeout_paused || timeout.remaining_duration.is_zero() {
|
||||
progress.into_any_element()
|
||||
} else {
|
||||
progress
|
||||
.with_animation(
|
||||
(
|
||||
"pending-keystrokes-countdown",
|
||||
render_state.pending_input_generation,
|
||||
),
|
||||
Animation::new(timeout.remaining_duration).with_max_fps(30.0),
|
||||
move |progress, delta| {
|
||||
progress.value(remaining_fraction * (1.0 - delta))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
KeyBinding::from_keystrokes(render_state.keystrokes.clone(), false)
|
||||
@@ -483,7 +502,10 @@ mod tests {
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
timeout_paused: render_state.timeout_paused,
|
||||
timeout_paused: render_state
|
||||
.timeout
|
||||
.as_ref()
|
||||
.is_some_and(|timeout| timeout.timeout_paused),
|
||||
popover_visible: indicator.popover.visible,
|
||||
popover_pointer_over: indicator.popover.pointer_over,
|
||||
})
|
||||
@@ -546,6 +568,12 @@ mod tests {
|
||||
cx.simulate_mouse_move(outside, None, Modifiers::none());
|
||||
}
|
||||
|
||||
fn start_popover_dismissal(cx: &mut VisualTestContext) {
|
||||
start_pending_input_and_hover_indicator(cx);
|
||||
move_pointer_over_popover(cx);
|
||||
move_pointer_outside(cx);
|
||||
}
|
||||
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
@@ -571,6 +599,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_indicator_stays_hidden_without_pending_input(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, timed_bindings());
|
||||
cx.run_until_parked();
|
||||
|
||||
// Before any input, neither the indicator's state nor its rendered element should exist.
|
||||
cx.update(|window, _| assert!(!window.has_pending_keystrokes()));
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_none()));
|
||||
assert!(cx.debug_bounds("PENDING_KEYSTROKES_INDICATOR").is_none());
|
||||
|
||||
// "x" starts no chord in this keymap, so ordinary input must keep the indicator hidden.
|
||||
cx.simulate_keystrokes("x");
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|window, _| assert!(!window.has_pending_keystrokes()));
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_none()));
|
||||
assert!(cx.debug_bounds("PENDING_KEYSTROKES_INDICATOR").is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_indicator_tracks_pending_input(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, nested_timed_bindings());
|
||||
@@ -801,23 +848,176 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_popover_hides_and_timeout_resumes_after_delay(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, nested_timed_bindings());
|
||||
fn test_hover_pauses_timeout_when_untimed_input_becomes_timed(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) =
|
||||
setup_indicator_test(cx, nested_timed_bindings().into_iter().skip(1));
|
||||
start_pending_input_and_hover_indicator(cx);
|
||||
move_pointer_over_popover(cx);
|
||||
cx.update(|window, _| {
|
||||
assert!(
|
||||
window
|
||||
.pending_input()
|
||||
.expect("untimed prefix")
|
||||
.timeout()
|
||||
.is_none()
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("h");
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| {
|
||||
assert!(
|
||||
window
|
||||
.pending_input()
|
||||
.expect("timed prefix")
|
||||
.timeout()
|
||||
.expect("new timeout")
|
||||
.is_paused()
|
||||
);
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(Duration::from_secs(2));
|
||||
cx.run_until_parked();
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_some()));
|
||||
cx.update(|window, _| assert!(window.has_pending_keystrokes()));
|
||||
|
||||
move_pointer_outside(cx);
|
||||
cx.executor().advance_clock(POPOVER_HIDE_DELAY);
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| {
|
||||
assert!(
|
||||
!window
|
||||
.pending_input()
|
||||
.expect("pending input after leaving popover")
|
||||
.timeout()
|
||||
.expect("resumed timeout")
|
||||
.is_paused()
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("j");
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| assert!(!window.has_pending_keystrokes()));
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_none()));
|
||||
assert!(cx.debug_bounds("PENDING_KEYSTROKES_POPOVER").is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_timed_chord_progress_dismisses_popover_during_dismissal_delay(cx: &mut TestAppContext) {
|
||||
// The standalone ctrl-b binding gives the initial prefix a timeout, which hovering pauses.
|
||||
assert_chord_progress_dismisses_popover(cx, nested_timed_bindings());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_newly_timed_chord_progress_dismisses_popover_during_dismissal_delay(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
// Without standalone ctrl-b, the initial prefix has no timeout. Pressing h creates one
|
||||
// because ctrl-b h is both a complete binding and a prefix of ctrl-b h j.
|
||||
assert_chord_progress_dismisses_popover(cx, nested_timed_bindings().into_iter().skip(1));
|
||||
}
|
||||
|
||||
fn assert_chord_progress_dismisses_popover(
|
||||
cx: &mut TestAppContext,
|
||||
bindings: impl IntoIterator<Item = KeyBinding>,
|
||||
) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, bindings);
|
||||
start_popover_dismissal(cx);
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
|
||||
// The pointer has left, but only 100 ms of the 300 ms dismissal delay has elapsed.
|
||||
indicator.read_with(cx, |indicator, _| {
|
||||
assert!(indicator.popover.visible);
|
||||
assert!(indicator.popover.hide_task.is_some());
|
||||
});
|
||||
|
||||
// Continuing the chord must close the popover now, without waiting for the delay.
|
||||
cx.simulate_keystrokes("h");
|
||||
cx.run_until_parked();
|
||||
|
||||
indicator.read_with(cx, |indicator, _| {
|
||||
assert!(!indicator.popover.visible);
|
||||
assert!(indicator.popover.hide_task.is_none());
|
||||
});
|
||||
|
||||
// Both a previously paused timeout and a newly created one must run for a full
|
||||
// second from this keypress, without waiting for the popover's dismissal delay.
|
||||
cx.update(|window, cx| {
|
||||
let timeout = window
|
||||
.pending_input()
|
||||
.expect("pending chord")
|
||||
.timeout()
|
||||
.expect("timed chord");
|
||||
assert!(!timeout.is_paused());
|
||||
assert_eq!(timeout.remaining(cx), Duration::from_secs(1));
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(999));
|
||||
cx.run_until_parked();
|
||||
|
||||
// The chord must still be pending just before its one-second timeout.
|
||||
cx.update(|window, _| assert!(window.has_pending_keystrokes()));
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(1));
|
||||
cx.run_until_parked();
|
||||
|
||||
// At exactly one second, the timeout must clear pending input and the indicator.
|
||||
cx.update(|window, _| assert!(!window.has_pending_keystrokes()));
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_none()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_timeout_notifications_preserve_popover_dismissal_delay(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, nested_timed_bindings());
|
||||
start_popover_dismissal(cx);
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
|
||||
// Trigger notifications without changing the pending keys. They must neither
|
||||
// close the popover early nor restart its 300 ms dismissal delay.
|
||||
cx.update(|window, cx| {
|
||||
assert!(window.set_pending_input_timeout_paused(&indicator, false, cx));
|
||||
assert!(window.set_pending_input_timeout_paused(&indicator, true, cx));
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.popover.visible));
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(199));
|
||||
cx.run_until_parked();
|
||||
|
||||
// At 299 ms since the pointer left, the popover must not have closed early.
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.popover.visible));
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(1));
|
||||
cx.run_until_parked();
|
||||
|
||||
// It must close at the original 300 ms deadline. Restarting the delay when
|
||||
// the notifications arrived at 100 ms would leave it open until 400 ms.
|
||||
assert!(!indicator.read_with(cx, |indicator, _| indicator.popover.visible));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_popover_hides_and_timeout_resumes_after_delay(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(cx, nested_timed_bindings());
|
||||
start_popover_dismissal(cx);
|
||||
|
||||
cx.executor()
|
||||
.advance_clock(POPOVER_HIDE_DELAY - Duration::from_millis(1));
|
||||
cx.run_until_parked();
|
||||
let grace_period_render_state = indicator
|
||||
|
||||
let dismissing_render_state = indicator
|
||||
.read_with(cx, |indicator, _| indicator_snapshot(indicator))
|
||||
.expect("pending input during popover dismissal grace period");
|
||||
assert!(grace_period_render_state.timeout_paused);
|
||||
assert!(grace_period_render_state.popover_visible);
|
||||
.expect("pending input during popover dismissal delay");
|
||||
assert!(dismissing_render_state.timeout_paused);
|
||||
assert!(dismissing_render_state.popover_visible);
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(1));
|
||||
cx.run_until_parked();
|
||||
|
||||
let resumed_render_state = indicator
|
||||
.read_with(cx, |indicator, _| indicator_snapshot(indicator))
|
||||
.expect("resumed pending input");
|
||||
@@ -862,7 +1062,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_indicator_ignores_pending_input_without_timeout(cx: &mut TestAppContext) {
|
||||
fn test_indicator_shows_pending_input_without_timeout(cx: &mut TestAppContext) {
|
||||
let (indicator, _, cx) = setup_indicator_test(
|
||||
cx,
|
||||
[KeyBinding::new(
|
||||
@@ -890,7 +1090,40 @@ mod tests {
|
||||
let pending_input = window.pending_input().expect("pending input");
|
||||
assert!(pending_input.timeout().is_none());
|
||||
});
|
||||
// Untimed input must still show the pending keys and matching binding,
|
||||
// without countdown state.
|
||||
let snapshot = indicator
|
||||
.read_with(cx, |indicator, _| {
|
||||
assert!(
|
||||
indicator
|
||||
.render_state()
|
||||
.expect("pending input")
|
||||
.timeout
|
||||
.is_none()
|
||||
);
|
||||
indicator_snapshot(indicator)
|
||||
})
|
||||
.expect("pending input snapshot");
|
||||
assert_eq!(snapshot.keystrokes, vec!["ctrl-b"]);
|
||||
assert_eq!(
|
||||
snapshot.bindings,
|
||||
vec![(
|
||||
vec!["h".to_string()],
|
||||
humanize_action_name(LongerBinding.name()),
|
||||
)]
|
||||
);
|
||||
assert!(cx.debug_bounds("PENDING_KEYSTROKES_INDICATOR").is_some());
|
||||
assert!(notification_count.get() > 0);
|
||||
|
||||
// Waiting longer than the usual one-second timeout must not hide an untimed chord.
|
||||
cx.executor().advance_clock(Duration::from_secs(2));
|
||||
cx.run_until_parked();
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_some()));
|
||||
|
||||
// Completing the chord must clear pending input and hide the indicator.
|
||||
cx.simulate_keystrokes("h");
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, _| assert!(!window.has_pending_keystrokes()));
|
||||
assert!(indicator.read_with(cx, |indicator, _| indicator.render_state().is_none()));
|
||||
assert_eq!(notification_count.get(), 0);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ When this happens, and both bindings are active in the current context, Zed will
|
||||
|
||||
Zed can also wait before inserting printable text when it might begin a multi-stroke binding. For example, with a `j k` binding, typing `j` waits briefly for `k`; otherwise, `j` is inserted after the timeout.
|
||||
|
||||
Whenever pending input has a timeout, a countdown indicator with the pending keystrokes is shown in the status bar. Hovering it lists the bindings that could still match and pauses the timeout so you can read them. The timeout resumes with the same remaining duration when the pointer leaves. The indicator can be hidden with `{"status_bar": {"pending_keystrokes_indicator": false}}`. Vim and Helix modes continue to use their existing pending-key indicator instead.
|
||||
Whenever multi-stroke input is pending, an indicator with the pending keystrokes is shown in the status bar. Hovering it lists the bindings that could still match. If the input has a timeout, the indicator also shows a countdown, and hovering pauses the timeout so you can read the bindings. The timeout resumes with the same remaining duration when the pointer leaves. Without a timeout, the pending keystrokes are shown without a countdown. The indicator can be hidden with `{"status_bar": {"pending_keystrokes_indicator": false}}`. Vim and Helix modes continue to use their existing pending-key indicator instead.
|
||||
|
||||
To also use the larger which-key menu, open the Settings Editor and search for
|
||||
`Show Which-key Menu`. The pending keystrokes indicator remains visible when the
|
||||
|
||||
@@ -2036,7 +2036,7 @@ Positive `integer` value between 1 and 32. Values outside of this range will be
|
||||
- `cursor_position_button`: Whether to show the cursor position button (clicking it opens the go-to-line/column input)
|
||||
- `line_endings_button`: Whether to show the active line endings button (clicking it opens the line-ending selector)
|
||||
- `active_encoding_button`: When to show the active encoding button: `"enabled"`, `"disabled"`, or `"non_utf8"` (only for encodings other than UTF-8 without BOM)
|
||||
- `pending_keystrokes_indicator`: Whether to show an indicator with a countdown while timed multi-stroke input is pending. Hovering the indicator pauses the timeout. Its binding preview popover is disabled when the which-key popup is enabled (see [key bindings](../key-bindings.md#precedence))
|
||||
- `pending_keystrokes_indicator`: Whether to show an indicator while multi-stroke input is pending. If the input has a timeout, a countdown is shown and hovering the indicator pauses it. Its binding preview popover is disabled when the which-key popup is enabled (see [key bindings](../key-bindings.md#precedence))
|
||||
|
||||
There is an experimental setting that completely hides the status bar. This causes major usability problems (you will be unable to use many of Zed's features), but is provided for those who value screen real-estate above all else.
|
||||
|
||||
|
||||
@@ -361,9 +361,10 @@ TBD: Centered layout related settings
|
||||
// If set to "non_utf8", the button is hidden only for UTF-8 without BOM.
|
||||
// Defaults to "non_utf8".
|
||||
"active_encoding_button": "non_utf8",
|
||||
// Show/hide an indicator with a countdown while timed multi-stroke
|
||||
// input is pending. Hovering it pauses the timeout. Unless the which-key
|
||||
// menu is enabled, hovering also lists the bindings that could still match.
|
||||
// Show/hide an indicator while multi-stroke input is pending.
|
||||
// If the input has a timeout, a countdown is shown and hovering pauses it.
|
||||
// Unless the which-key menu is enabled, hovering also lists the bindings
|
||||
// that could still match.
|
||||
// Defaults to true.
|
||||
"pending_keystrokes_indicator": true
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user