Ever since we launched the keymap editor, we've received feedback about how "_creating_" a new keybinding was not obvious. At first, I was confused about this feedback because you can't "create" a keybinding, you need to rather _assign it_ to an action... so what always made sense to me was to start with searching for the action, which is an use case we already support. Regardless, having an easy to reach "create" button, which essentially still asks for action > keystroke > arguments (if needed) > and context, feels like a win UX-wise; a bit of a redundant flow that feels ultimately positive. So, this PR adds a "Create Keybinding" button to the keymap editor, which you can reach with `cmd-k`. That will open up a modal with an action autocomplete, the keystroke recording input, the arguments input (if needed), and the context input. https://github.com/user-attachments/assets/86f64314-4685-47bb-bb0d-72ca4c469d1f Release Notes: - keymap editor: Added a keybinding creation modal to make it easier to assign an action to a keystroke.
137 lines
4.4 KiB
Rust
137 lines
4.4 KiB
Rust
use collections::HashMap;
|
|
use command_palette;
|
|
use editor::{CompletionProvider, Editor};
|
|
use fuzzy::StringMatchCandidate;
|
|
use gpui::{Context, Entity, SharedString, Window};
|
|
use language::{self, ToOffset};
|
|
use project::{self, CompletionDisplayOptions};
|
|
|
|
pub struct ActionCompletionProvider {
|
|
action_names: Vec<&'static str>,
|
|
humanized_names: HashMap<&'static str, SharedString>,
|
|
}
|
|
|
|
impl ActionCompletionProvider {
|
|
pub fn new(
|
|
action_names: Vec<&'static str>,
|
|
humanized_names: HashMap<&'static str, SharedString>,
|
|
) -> Self {
|
|
Self {
|
|
action_names,
|
|
humanized_names,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletionProvider for ActionCompletionProvider {
|
|
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 == '_' || 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;
|
|
let snapshot = buffer.text_snapshot();
|
|
let query: String = snapshot.text_for_range(replace_range.clone()).collect();
|
|
let normalized_query = command_palette::normalize_action_query(&query);
|
|
|
|
let candidates: Vec<StringMatchCandidate> = self
|
|
.action_names
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(ix, &name)| {
|
|
let humanized = self
|
|
.humanized_names
|
|
.get(name)
|
|
.cloned()
|
|
.unwrap_or_else(|| name.into());
|
|
StringMatchCandidate::new(ix, &humanized)
|
|
})
|
|
.collect();
|
|
|
|
let executor = cx.background_executor().clone();
|
|
let executor_for_fuzzy = executor.clone();
|
|
let action_names = self.action_names.clone();
|
|
let humanized_names = self.humanized_names.clone();
|
|
|
|
executor.spawn(async move {
|
|
let matches = fuzzy::match_strings(
|
|
&candidates,
|
|
&normalized_query,
|
|
true,
|
|
true,
|
|
action_names.len(),
|
|
&Default::default(),
|
|
executor_for_fuzzy,
|
|
)
|
|
.await;
|
|
|
|
let completions: Vec<project::Completion> = matches
|
|
.iter()
|
|
.take(50)
|
|
.map(|m| {
|
|
let action_name = action_names[m.candidate_id];
|
|
let humanized = humanized_names
|
|
.get(action_name)
|
|
.cloned()
|
|
.unwrap_or_else(|| action_name.into());
|
|
|
|
project::Completion {
|
|
replace_range: replace_range.clone(),
|
|
label: language::CodeLabel::plain(humanized.to_string(), None),
|
|
new_text: action_name.to_string(),
|
|
documentation: None,
|
|
source: project::CompletionSource::Custom,
|
|
icon_path: None,
|
|
match_start: None,
|
|
snippet_deduplication_key: None,
|
|
insert_text_mode: None,
|
|
confirm: None,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(vec![project::CompletionResponse {
|
|
completions,
|
|
display_options: CompletionDisplayOptions {
|
|
dynamic_width: true,
|
|
},
|
|
is_incomplete: false,
|
|
}])
|
|
})
|
|
}
|
|
|
|
fn is_completion_trigger(
|
|
&self,
|
|
_buffer: &Entity<language::Buffer>,
|
|
_position: language::Anchor,
|
|
text: &str,
|
|
_trigger_in_words: bool,
|
|
_cx: &mut Context<Editor>,
|
|
) -> bool {
|
|
text.chars().last().is_some_and(|last_char| {
|
|
last_char.is_ascii_alphanumeric() || last_char == '_' || last_char == ':'
|
|
})
|
|
}
|
|
}
|