From 877ef5e1b1fcfeabe8cdd203239d2738a0991f19 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 7 Jul 2025 16:54:51 -0500 Subject: [PATCH] keymap_ui: Add auto-complete for context in keybind editor (#34031) Closes #ISSUE Implements a very basic completion provider that is attached to the context editor in the keybind editing modal. The context identifiers used for completions are scraped from the default, vim, and base keymaps on demand. Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 - .../src/base_keymap_setting.rs | 4 +- crates/settings/src/keymap_file.rs | 2 +- crates/settings/src/settings.rs | 3 + crates/settings_ui/src/keybindings.rs | 152 +++++++++++++++++- crates/welcome/Cargo.toml | 1 - crates/welcome/src/base_keymap_picker.rs | 3 +- crates/welcome/src/welcome.rs | 4 - crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 9 +- 10 files changed, 163 insertions(+), 20 deletions(-) rename crates/{welcome => settings}/src/base_keymap_setting.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index a19397bdf909226095943735d3afd34ac80723e8..58e482ee39bc5d89870671b77a1bfdd5a4762e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18359,7 +18359,6 @@ dependencies = [ "language", "picker", "project", - "schemars", "serde", "settings", "telemetry", diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs similarity index 96% rename from crates/welcome/src/base_keymap_setting.rs rename to crates/settings/src/base_keymap_setting.rs index b841b69f9d25c5b84d46b0b192f8dffd9929fe02..6916d98ae336629df1ebd2ba58be4f5491b8bd82 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,8 +1,8 @@ use std::fmt::{Display, Formatter}; +use crate::{Settings, SettingsSources, VsCodeSettings}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// @@ -114,7 +114,7 @@ impl Settings for BaseKeymap { sources.default.ok_or_else(Self::missing_default) } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) { *current = Some(BaseKeymap::VSCode); } } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index ca54b6a877361af15a634ec7ce3c247ffeaff49f..b91739ca87c72c13c1cfe283b80c8ef260c31f6a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,7 +63,7 @@ pub struct KeymapSection { /// current file extension are also supported - see [the /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details. #[serde(default)] - context: String, + pub context: String, /// This option enables specifying keys based on their position on a QWERTY keyboard, by using /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported /// on macOS. See the documentation for more details. diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f690a2ea936c6516b6d4a60701a7cce89fa50cb2..4e6bd94d92bc09009b2775b7b43f9962341c5f94 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,4 @@ +mod base_keymap_setting; mod editable_setting_control; mod key_equivalents; mod keymap_file; @@ -11,6 +12,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, fmt, str}; use util::asset_str; +pub use base_keymap_setting::*; pub use editable_setting_control::*; pub use key_equivalents::*; pub use keymap_file::{ @@ -71,6 +73,7 @@ pub fn init(cx: &mut App) { .set_default_settings(&default_settings(), cx) .unwrap(); cx.set_global(settings); + BaseKeymap::register(cx); } pub fn default_settings() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 34d4b8585256d12b62b725d360679225b7360a82..2dd693c798a7a4c91eb89d9d404cc2becf143197 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::{Context as _, anyhow}; use collections::HashSet; -use editor::{Editor, EditorEvent}; +use editor::{CompletionProvider, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -14,8 +14,8 @@ use gpui::{ Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, div, transparent_black, }; -use language::{Language, LanguageConfig}; -use settings::KeybindSource; +use language::{Language, LanguageConfig, ToOffset as _}; +use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; @@ -850,8 +850,10 @@ impl KeybindingEditorModal { cx: &mut App, ) -> Self { let keybind_editor = cx.new(KeystrokeInput::new); + let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); + if let Some(context) = editing_keybind .context .as_ref() @@ -862,6 +864,21 @@ impl KeybindingEditorModal { editor.set_placeholder_text("Keybinding context", cx); } + cx.spawn(async |editor, cx| { + let contexts = cx + .background_spawn(async { collect_contexts_from_assets() }) + .await; + + editor + .update(cx, |editor, _cx| { + editor.set_completion_provider(Some(std::rc::Rc::new( + KeyContextCompletionProvider { contexts }, + ))); + }) + .context("Failed to load completions for keybinding context") + }) + .detach_and_log_err(cx); + editor }); Self { @@ -1001,6 +1018,69 @@ impl Render for KeybindingEditorModal { } } +struct KeyContextCompletionProvider { + contexts: Vec, +} + +impl CompletionProvider for KeyContextCompletionProvider { + fn completions( + &self, + _excerpt_id: editor::ExcerptId, + buffer: &Entity, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> gpui::Task>> { + let buffer = buffer.read(cx); + let mut count_back = 0; + for char in buffer.reversed_chars_at(buffer_position) { + if char.is_ascii_alphanumeric() || char == '_' { + count_back += 1; + } else { + break; + } + } + let start_anchor = buffer.anchor_before( + buffer_position + .to_offset(&buffer) + .saturating_sub(count_back), + ); + let replace_range = start_anchor..buffer_position; + gpui::Task::ready(Ok(vec![project::CompletionResponse { + completions: self + .contexts + .iter() + .map(|context| project::Completion { + replace_range: replace_range.clone(), + label: language::CodeLabel::plain(context.to_string(), None), + new_text: context.to_string(), + documentation: None, + source: project::CompletionSource::Custom, + icon_path: None, + insert_text_mode: None, + confirm: None, + }) + .collect(), + is_incomplete: false, + }])) + } + + fn is_completion_trigger( + &self, + _buffer: &Entity, + _position: language::Anchor, + text: &str, + _trigger_in_words: bool, + _menu_is_open: bool, + _cx: &mut Context, + ) -> bool { + text.chars().last().map_or(false, |last_char| { + last_char.is_ascii_alphanumeric() || last_char == '_' + }) + } +} + async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], @@ -1254,6 +1334,72 @@ fn build_keybind_context_menu( }) } +fn collect_contexts_from_assets() -> Vec { + let mut keymap_assets = vec![ + util::asset_str::(settings::DEFAULT_KEYMAP_PATH), + util::asset_str::(settings::VIM_KEYMAP_PATH), + ]; + keymap_assets.extend( + BaseKeymap::OPTIONS + .iter() + .filter_map(|(_, base_keymap)| base_keymap.asset_path()) + .map(util::asset_str::), + ); + + let mut contexts = HashSet::default(); + + for keymap_asset in keymap_assets { + let Ok(keymap) = KeymapFile::parse(&keymap_asset) else { + continue; + }; + + for section in keymap.sections() { + let context_expr = §ion.context; + let mut queue = Vec::new(); + let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else { + continue; + }; + + queue.push(root_context); + while let Some(context) = queue.pop() { + match context { + gpui::KeyBindingContextPredicate::Identifier(ident) => { + contexts.insert(ident); + } + gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Not(ctx) => { + queue.push(*ctx); + } + gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + } + } + } + } + + let mut contexts = contexts.into_iter().collect::>(); + contexts.sort(); + + return contexts; +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 6d4896016c7106527ecfe3c7a89da65c1126dc19..769dd8d6aa1591b2bd9c62dcb8c9ad9e48ad457b 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -26,7 +26,6 @@ install_cli.workspace = true language.workspace = true picker.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true telemetry.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index d5a6ae96da1345f4cefe6ac722e040cc82192f26..92317ca7113aee06025d2490656e869c7b4a5b0a 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,4 +1,3 @@ -use super::base_keymap_setting::BaseKeymap; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{ App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, @@ -6,7 +5,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use project::Fs; -use settings::{Settings, update_settings_file}; +use settings::{BaseKeymap, Settings, update_settings_file}; use std::sync::Arc; use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 74d7323d8cea4f0b427c1a0cb3ac1291d838baee..ea4ac13de7f41f4b46da6b465f1e22076a5bae7b 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -17,11 +17,9 @@ use workspace::{ open_new, }; -pub use base_keymap_setting::BaseKeymap; pub use multibuffer_hint::*; mod base_keymap_picker; -mod base_keymap_setting; mod multibuffer_hint; mod welcome_ui; @@ -37,8 +35,6 @@ pub const FIRST_OPEN: &str = "first_open"; pub const DOCS_URL: &str = "https://zed.dev/docs/"; pub fn init(cx: &mut App) { - BaseKeymap::register(cx); - cx.observe_new(|workspace: &mut Workspace, _, _cx| { workspace.register_action(|workspace, _: &Welcome, window, cx| { let welcome_page = WelcomePage::new(workspace, cx); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e04e9c38c15b7ed1bc95bffc0d702b013150b3a5..3c46c486a8abce4db926aa650bd349d92eaecd9c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,7 +29,7 @@ use project::project_settings::ProjectSettings; use recent_projects::{SshSettings, open_ssh_project}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; -use settings::{Settings, SettingsStore, watch_config_file}; +use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ env, io::{self, IsTerminal}, @@ -43,7 +43,7 @@ use theme::{ }; use util::{ConnectionResult, ResultExt, TryFutureExt, maybe}; use uuid::Uuid; -use welcome::{BaseKeymap, FIRST_OPEN, show_welcome_view}; +use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 10fdcf34a6a1de867668163f15fd3dfe0434f09c..dc094a6c12fb1ba11642cc988f5d06d2cce01078 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -48,9 +48,10 @@ use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult, - Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content, - initial_project_settings_content, initial_tasks_content, update_settings_file, + BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, + KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH, + initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, + update_settings_file, }; use std::path::PathBuf; use std::sync::atomic::{self, AtomicBool}; @@ -62,7 +63,7 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use welcome::{BaseKeymap, DOCS_URL, MultibufferHint}; +use welcome::{DOCS_URL, MultibufferHint}; use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,