Conversation
|
Looking good so far. One issue I think is that you are showing all following actions, but I think it would be preferable to show only the next key in the sequence. For example, in the first picture showing
In my view, you should be showing only
As an aside on design, I think the key press and the command it yields should be distinguishable by color and / or font weight. |
|
@theherk Good call for showing only the next vs showing all. I'll add support for that and make it configurable. It does currently support multi-levels (as in I've already got font weight for keys locally, plus some other minor design/usability chances + configuration options. I'll push that tomorrow and update the screenshots. |
|
Hey @Cretezy, this is a wanted feature. Congrats on submitting that! In terms of UI, I recommend increasing highlight in the key and reducing it in the description. I suppose you could add some text or indication to make the current key clearer. I'm not sure how difficult it would be to group things. In your case, we can see when you press Take a look at the Lazyvim version of that. I hope you can finish that and your PR is merged soon. Thanks a lot. |
|
Pushed new commit with a better design (screenshot updated), as well as configuration option. I'll tackle grouping of sub-sequences next. |
f8a8f42 to
76b14cc
Compare
|
@domi413 Yes those improvements are planned, still working on the design! |
|
Nice! CC @danilo-leal for some design love. I think we only really need one setting (which is disabled by default), and which shows the data in what we think is the best way (not yet sure if that's the full key sequence or just the next key). One thing that's maybe different in Zed vs Helix is that we have way more shortcuts showing up in here, so if we can figure out how to keep the UI from taking over the entire screen it would be nice... In terms of the 1s timeout, we get a lot of feedback about that. We could try removing it completely, but I'm worried about people hitting |
|
@ConradIrwin Regarding the 1s, a few thoughts:
|
|
EDIT: if this timeout is not about how long the menu shows before you click the next key, then my comment can be ignored
As a helix user I would like the option to disable the timeout entirely when the menu is visible. I wouldn't want the menu to disappear whilst reading it, and any usable timeout would lead to that sometimes. I could of course live with a workaround where I can specify the timeout length, as I could just set it to 30 minutes or something. |
|
Hmm. Looking into vim, they have the 1s timeout when there are ambiguous key matches (e.g. do VScode seems closer to 5s by default, but I don't think you can have the key prefix matches (it seems to always use the shorter match instantly; though I may have missed how to configure the conflict correctly). It seems reasonable to split the two cases up:
|
bd0ada3 to
820b239
Compare
|
I've pushed another update which overhauls the design and introduces grouping (screenshots are updated). Still work to do, but I'm happy to collect feedback at this point. |
820b239 to
f24cc9f
Compare
| h_flex().items_center().gap_1().children([ | ||
| keystroke.into_any_element(), | ||
| div() | ||
| .child(Label::new("🡒").color(Color::Muted)) |
There was a problem hiding this comment.
Likely best to replace this with a proper icon
f24cc9f to
6813fb0
Compare
|
The group feature is a great start, but it would be even better if it was possible to document a group. If you look at helix, when you hit a key, you get a documented list of next keys to press. for example, when you hit «m» you get the popup for match mode. Then you among other see «i» for inside and «a» for around. When you press that you get «f» for function and «b» for block. And so on. With the current grouping, only the last keepers would have any sort of documentation. |
|
@bjorn-ove Agreed! I'm not sure if this belongs in the settings file or keymap file yet. I think keymap makes more sense, although it doesn't fit in the current model of the file. If it's in the settings, it could be under |
|
@Cretezy, another suggestion. Instead of
|
|
I’ve noticed that when the cursor is positioned on the bottom half of the screen, the which-key panel can end up obscuring it. There are two possible solutions that come to mind: A) Leave the current behavior as is. |
|
Few points:
|
|
To show a modal you can do: if !workspace.has_active_modal() {
workspace.toggle_modal(|window, cx| WhichKey::new(window, cx))
}I'm still sure we shouldn't ship column mode. Two reasons:
So yes, in the abstract, more flexibility is good; but it's better to have a tighter scoped feature that works well than a loose collection of things that kind of work. Removing the timeout completely seems reasonable, it sounded like from your comments that that has effectively already happened, we just need to tidy the code up? |
|
@ConradIrwin I've moved the implementation to a modal. It should basically behave as before (hides when there's no pending keys or when focus changes), plus it won't conflict with other modals. I've also removed the timer. Additionally, I've discovered a small bug with the mode indicator: The pending keys are still shown when focus changes (which clears them at the moment). |
crates/workspace/src/workspace.rs
Outdated
| _items_serializer: Task<Result<()>>, | ||
| session_id: Option<String>, | ||
| scheduled_tasks: Vec<Task<()>>, | ||
| pub which_key: Option<AnyEntity>, |
There was a problem hiding this comment.
I'm not 100% sure if this is the correct approach to preventing WhichKey from being dropped.
layout_as_root is quite expensive, so render() was taking ~16ms in a debug build for `g`. Without too much loss of accuracy, we now just layout as root once per column
|
I pushed up a few changes, notably using a closure to retain the which key state so we really don't need to change anything in the workspace crate; and removing the new I also made some changes to render. It turns out that Finally I realized that we do need to keep the default timer in the case of conflicting shorter bindings, for example if you have: If you hit |
|
@ConradIrwin I think we can also revert the changes with |
|
Perfect. Are you going to take the next step, or should I? |
|
Hmm, If we used the buffer font, it's probably monospace? Not sure that will look good though. For the timeout thing:
|
|
@Cretezy Have you had time to work on this recently? Happy to pair on the timeout thing if you need |
|
@ConradIrwin I haven't, I'm still a bit stuck at the timeout. I booked some time on your calendar this Friday to pair, hopefully we can finish this up. |
There was a problem hiding this comment.
Hello @Cretezy @ConradIrwin , thanks for all your contributions. I am very interested in making this PR to work so I came across all the commits and I suggest to make some changes here which I left them in the comments. The goal is to reflect the new changes in the upstream main branch in order to make the which-key feature working. Thank you.
There was a problem hiding this comment.
This refactor is not super relevant to this specific PR for adding which-key system. I recommend we can clean this up in another commit to avoid confusion. We can leave it as is for now and this will not cause any problems.
There was a problem hiding this comment.
The same comment apply here. The corresponding refactors in project_search.rs and collab.rs are not relevant to this PR.
There was a problem hiding this comment.
To reflect the new changes from the upstream:main branch. I made some changes in which key settings accordingly as follows:
crates/which_key/src/which_key_settings.rs
use serde::Deserialize;
use settings::{Settings, SettingsContent, SettingsKey, WhichKeySettingsContent};
#[derive(Deserialize)]
pub struct WhichKeySettings {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_delay_ms")]
pub delay_ms: u64,
}
fn default_delay_ms() -> u64 {
700
}
impl Settings for WhichKeySettings {
fn from_settings(content: &SettingsContent) -> Self {
let which_key: WhichKeySettingsContent = content.which_key.clone().unwrap_or_default();
Self {
enabled: which_key.enabled.unwrap_or(false),
delay_ms: which_key.delay_ms.unwrap_or_else(default_delay_ms),
}
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {
// No equivalent setting in VScode
}
}
impl SettingsKey for WhichKeySettings {
const KEY: Option<&'static str> = Some("which_key");
}crates/settings/src/settings_content.rs
pub struct SettingsContent {
...
/// Settings for the which-key popup.
pub which_key: Option<WhichKeySettingsContent>,
...
}
...
/// Settings for configuring the which-key popup behaviour.
#[skip_serializing_none]
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct WhichKeySettingsContent {
/// Whether to show the which-key popup when holding down key combinations
///
/// Default: false
pub enabled: Option<bool>,
/// Delay in milliseconds before showing the which-key popup.
///
/// Default: 700
pub delay_ms: Option<u64>,
}|
User customization of the layout/order (for grouping similar commands together)/description would be a great improvement For example, you could add icons to your commands in your user config "icon" field for better readability (so add an "icon" field to keybind config (Similar to #34798 (comment)) In a better config system you could even have it as simple as ⎈w⎈j pane::SplitDown i=🗗↓ des="Split ↓ (Horizontal)" №=3With your whic-key showing you Then you could also have a "cheatsheet" version popup first: instead of a huge table with a lot of text you could only show a "keyboard layout" view with icons, any only if that's still not enough you could mouseover for a detailed tooltip or press a key to show the full text table the ability to search the table would also help in long tables |
|
I think we should probably pause on this branch (as it's going stale) and open a first PR to fix the key timeout problem. Currently if you type a key that is a prefix of a keyboard shortcut and then don't finish the shortcut it is cancelled after 1 second. We should change that logic to keep it pending forever (or until escape is pressed) in the case that there is no binding (and no input). For things like Once we have that problem solved, then the remaining work is "just render the UI" and we can re-use the code from this branch to do that. Going to close for now, but I am still interested in PRs for this (as it is very highly requested), and I think the code here is heading the right direction (it's just very conflicted). |
|
Hi, I've been on vacation for the last few weeks, and should be able to get back to work on this soon. I'll focus on updating the PR, and try to meet with @ConradIrwin to discuss the timeout issue when possible. |
…#42659) Part One for Resolving #10910 ### Summary Typing prefix (partial keybinding) will behave like Vim. No timeout until you either finish the sequence or hit Escape, while ambiguous sequences still auto-resolve after 1s. ### Description This follow-up tweaks the which-key system first part groundwork so our timeout behavior matches Vim’s expectations. Then we can implement the UI part in the next step (reference latest comments in #34798) - `DispatchResult` now reports when the current keystrokes are already a complete binding in the active context stack (`pending_has_binding`). We only start the 1s flush timer in that case. Pure prefixes or sequences that only match in other contexts—stay pending indefinitely, so leader-style combos like `space f g` no longer evaporate after a second. - `Window::dispatch_key_event` cancels any prior timer before scheduling a new one and only spawns the background flush task when `pending_has_binding` is true. If there’s no matching binding, we keep the pending keystrokes and rely on an explicit Escape or more typing to resolve them. Release Notes: - Fixed multi-stroke keybindings so only ambiguous prefixes auto-trigger after 1 s; unmatched prefixes now stay pending until canceled, matching Vim-style leader behavior.
…zed-industries#42659) Part One for Resolving zed-industries#10910 ### Summary Typing prefix (partial keybinding) will behave like Vim. No timeout until you either finish the sequence or hit Escape, while ambiguous sequences still auto-resolve after 1s. ### Description This follow-up tweaks the which-key system first part groundwork so our timeout behavior matches Vim’s expectations. Then we can implement the UI part in the next step (reference latest comments in zed-industries#34798) - `DispatchResult` now reports when the current keystrokes are already a complete binding in the active context stack (`pending_has_binding`). We only start the 1s flush timer in that case. Pure prefixes or sequences that only match in other contexts—stay pending indefinitely, so leader-style combos like `space f g` no longer evaporate after a second. - `Window::dispatch_key_event` cancels any prior timer before scheduling a new one and only spawns the background flush task when `pending_has_binding` is true. If there’s no matching binding, we keep the pending keystrokes and rely on an explicit Escape or more typing to resolve them. Release Notes: - Fixed multi-stroke keybindings so only ambiguous prefixes auto-trigger after 1 s; unmatched prefixes now stay pending until canceled, matching Vim-style leader behavior.







Closes #10910
This is a work in progress implementation, but it functionally works. I'm looking for some feedback on both implementation and design.
This PR adds a
which-key-like system, which shows possible bindings to take after starting a sequence of key which leads to a binding. This is mostly useful in Vim mode, as there's multiple common sequences (such asgwhich leads togv,gc, ..., or[/], etc). Currently, this shows in the main screen area (doesn't draw over panels), to be less intrusive.Common use cases are to see what can follow
ctrl+k,g,z,ctrl+w, orspace/leader.g/ctrl+ware worst case scenarios, as they have tons of features built-in."location": "left_panel":To do:
Additionally, there's some follow-up work which could enable this menu when holding down
ctrlfor example. This would make this feature much more useful in non-Vim mode.Release Notes:
ctrl-w)