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 ...
This commit is contained in:
Ben Kunkle 2025-07-07 16:54:51 -05:00 committed by GitHub
parent 66a1c356bf
commit 877ef5e1b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 163 additions and 20 deletions

View file

@ -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<SharedString>,
}
impl CompletionProvider for KeyContextCompletionProvider {
fn completions(
&self,
_excerpt_id: editor::ExcerptId,
buffer: &Entity<language::Buffer>,
buffer_position: language::Anchor,
_trigger: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
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<language::Buffer>,
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
_cx: &mut Context<Editor>,
) -> 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<SharedString> {
let mut keymap_assets = vec![
util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
];
keymap_assets.extend(
BaseKeymap::OPTIONS
.iter()
.filter_map(|(_, base_keymap)| base_keymap.asset_path())
.map(util::asset_str::<SettingsAssets>),
);
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 = &section.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::<Vec<_>>();
contexts.sort();
return contexts;
}
impl SerializableItem for KeymapEditor {
fn serialized_item_kind() -> &'static str {
"KeymapEditor"