keymap_ui: Fix various keymap editor issues (cherry-pick #34647) (#34670)

Cherry-picked keymap_ui: Fix various keymap editor issues (#34647)

This PR tackles miscellaneous nits for the new keymap editor UI.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>

Co-authored-by: Finn Evers <finn@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
gcp-cherry-pick-bot[bot] 2025-07-17 20:03:03 -04:00 committed by GitHub
parent c9b9b3194e
commit ce0de10147
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 246 additions and 158 deletions

View file

@ -22,9 +22,9 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets};
use util::ResultExt; use util::ResultExt;
use ui::{ use ui::{
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal, ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _, Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
Tooltip, Window, prelude::*, Styled as _, Tooltip, Window, prelude::*,
}; };
use ui_input::SingleLineInput; use ui_input::SingleLineInput;
use workspace::{ use workspace::{
@ -179,14 +179,29 @@ impl FilterState {
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
struct ActionMapping { struct ActionMapping {
keystroke_text: SharedString, keystrokes: Vec<Keystroke>,
context: Option<SharedString>, context: Option<SharedString>,
} }
#[derive(Debug)]
struct KeybindConflict {
first_conflict_index: usize,
remaining_conflict_amount: usize,
}
impl KeybindConflict {
fn from_iter<'a>(mut indices: impl Iterator<Item = &'a usize>) -> Option<Self> {
indices.next().map(|index| Self {
first_conflict_index: *index,
remaining_conflict_amount: indices.count(),
})
}
}
#[derive(Default)] #[derive(Default)]
struct ConflictState { struct ConflictState {
conflicts: Vec<usize>, conflicts: Vec<usize>,
action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>, keybind_mapping: HashMap<ActionMapping, Vec<usize>>,
} }
impl ConflictState { impl ConflictState {
@ -197,7 +212,7 @@ impl ConflictState {
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, binding)| { .filter(|(_, binding)| {
!binding.keystroke_text.is_empty() binding.keystrokes().is_some()
&& binding && binding
.source .source
.as_ref() .as_ref()
@ -217,27 +232,26 @@ impl ConflictState {
.flatten() .flatten()
.copied() .copied()
.collect(), .collect(),
action_keybind_mapping, keybind_mapping: action_keybind_mapping,
} }
} }
fn conflicting_indices_for_mapping( fn conflicting_indices_for_mapping(
&self, &self,
action_mapping: ActionMapping, action_mapping: &ActionMapping,
keybind_idx: usize, keybind_idx: usize,
) -> Option<Vec<usize>> { ) -> Option<KeybindConflict> {
self.action_keybind_mapping self.keybind_mapping
.get(&action_mapping) .get(action_mapping)
.and_then(|indices| { .and_then(|indices| {
let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable(); KeybindConflict::from_iter(indices.iter().filter(|&idx| *idx != keybind_idx))
indices.peek().is_some().then(|| indices.copied().collect())
}) })
} }
fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> { fn will_conflict(&self, action_mapping: &ActionMapping) -> Option<KeybindConflict> {
self.action_keybind_mapping self.keybind_mapping
.get(&action_mapping) .get(action_mapping)
.and_then(|indices| indices.is_empty().not().then_some(indices.clone())) .and_then(|indices| KeybindConflict::from_iter(indices.iter()))
} }
fn has_conflict(&self, candidate_idx: &usize) -> bool { fn has_conflict(&self, candidate_idx: &usize) -> bool {
@ -267,7 +281,7 @@ struct KeymapEditor {
selected_index: Option<usize>, selected_index: Option<usize>,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
previous_edit: Option<PreviousEdit>, previous_edit: Option<PreviousEdit>,
humanized_action_names: HashMap<&'static str, SharedString>, humanized_action_names: HumanizedActionNameCache,
show_hover_menus: bool, show_hover_menus: bool,
} }
@ -332,14 +346,6 @@ impl KeymapEditor {
}) })
.detach(); .detach();
let humanized_action_names =
HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
(
action_name,
command_palette::humanize_action_name(action_name).into(),
)
}));
let mut this = Self { let mut this = Self {
workspace, workspace,
keybindings: vec![], keybindings: vec![],
@ -356,8 +362,8 @@ impl KeymapEditor {
selected_index: None, selected_index: None,
context_menu: None, context_menu: None,
previous_edit: None, previous_edit: None,
humanized_action_names,
search_query_debounce: None, search_query_debounce: None,
humanized_action_names: HumanizedActionNameCache::new(cx),
show_hover_menus: true, show_hover_menus: true,
}; };
@ -383,6 +389,24 @@ impl KeymapEditor {
} }
} }
fn filter_on_selected_binding_keystrokes(&mut self, cx: &mut Context<Self>) {
let Some(selected_binding) = self.selected_binding() else {
return;
};
let keystrokes = selected_binding
.keystrokes()
.map(Vec::from)
.unwrap_or_default();
self.filter_state = FilterState::All;
self.search_mode = SearchMode::KeyStroke { exact_match: true };
self.keystroke_editor.update(cx, |editor, cx| {
editor.set_keystrokes(keystrokes, cx);
});
}
fn on_query_changed(&mut self, cx: &mut Context<Self>) { fn on_query_changed(&mut self, cx: &mut Context<Self>) {
let action_query = self.current_action_query(cx); let action_query = self.current_action_query(cx);
let keystroke_query = self.current_keystroke_query(cx); let keystroke_query = self.current_keystroke_query(cx);
@ -523,6 +547,7 @@ impl KeymapEditor {
fn process_bindings( fn process_bindings(
json_language: Arc<Language>, json_language: Arc<Language>,
zed_keybind_context_language: Arc<Language>, zed_keybind_context_language: Arc<Language>,
humanized_action_names: &HumanizedActionNameCache,
cx: &mut App, cx: &mut App,
) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) { ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
let key_bindings_ptr = cx.key_bindings(); let key_bindings_ptr = cx.key_bindings();
@ -570,12 +595,14 @@ impl KeymapEditor {
let action_docs = action_documentation.get(action_name).copied(); let action_docs = action_documentation.get(action_name).copied();
let index = processed_bindings.len(); let index = processed_bindings.len();
let string_match_candidate = StringMatchCandidate::new(index, &action_name); let humanized_action_name = humanized_action_names.get(action_name);
let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name);
processed_bindings.push(ProcessedKeybinding { processed_bindings.push(ProcessedKeybinding {
keystroke_text: keystroke_text.into(), keystroke_text: keystroke_text.into(),
ui_key_binding, ui_key_binding,
action_name, action_name,
action_arguments, action_arguments,
humanized_action_name,
action_docs, action_docs,
action_schema: action_schema.get(action_name).cloned(), action_schema: action_schema.get(action_name).cloned(),
context: Some(context), context: Some(context),
@ -587,12 +614,14 @@ impl KeymapEditor {
let empty = SharedString::new_static(""); let empty = SharedString::new_static("");
for action_name in unmapped_action_names.into_iter() { for action_name in unmapped_action_names.into_iter() {
let index = processed_bindings.len(); let index = processed_bindings.len();
let string_match_candidate = StringMatchCandidate::new(index, &action_name); let humanized_action_name = humanized_action_names.get(action_name);
let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name);
processed_bindings.push(ProcessedKeybinding { processed_bindings.push(ProcessedKeybinding {
keystroke_text: empty.clone(), keystroke_text: empty.clone(),
ui_key_binding: None, ui_key_binding: None,
action_name, action_name,
action_arguments: None, action_arguments: None,
humanized_action_name,
action_docs: action_documentation.get(action_name).copied(), action_docs: action_documentation.get(action_name).copied(),
action_schema: action_schema.get(action_name).cloned(), action_schema: action_schema.get(action_name).cloned(),
context: None, context: None,
@ -612,15 +641,15 @@ impl KeymapEditor {
load_keybind_context_language(workspace.clone(), cx).await; load_keybind_context_language(workspace.clone(), cx).await;
let (action_query, keystroke_query) = this.update(cx, |this, cx| { let (action_query, keystroke_query) = this.update(cx, |this, cx| {
let (key_bindings, string_match_candidates) = let (key_bindings, string_match_candidates) = Self::process_bindings(
Self::process_bindings(json_language, zed_keybind_context_language, cx); json_language,
zed_keybind_context_language,
&this.humanized_action_names,
cx,
);
this.keybinding_conflict_state = ConflictState::new(&key_bindings); this.keybinding_conflict_state = ConflictState::new(&key_bindings);
if !this.keybinding_conflict_state.any_conflicts() {
this.filter_state = FilterState::All;
}
this.keybindings = key_bindings; this.keybindings = key_bindings;
this.string_match_candidates = Arc::new(string_match_candidates); this.string_match_candidates = Arc::new(string_match_candidates);
this.matches = this this.matches = this
@ -751,10 +780,6 @@ impl KeymapEditor {
) { ) {
let weak = cx.weak_entity(); let weak = cx.weak_entity();
self.context_menu = self.selected_binding().map(|selected_binding| { self.context_menu = self.selected_binding().map(|selected_binding| {
let key_strokes = selected_binding
.keystrokes()
.map(Vec::from)
.unwrap_or_default();
let selected_binding_has_no_context = selected_binding let selected_binding_has_no_context = selected_binding
.context .context
.as_ref() .as_ref()
@ -784,17 +809,9 @@ impl KeymapEditor {
Box::new(CopyContext), Box::new(CopyContext),
) )
.entry("Show matching keybindings", None, { .entry("Show matching keybindings", None, {
let weak = weak.clone();
let key_strokes = key_strokes.clone();
move |_, cx| { move |_, cx| {
weak.update(cx, |this, cx| { weak.update(cx, |this, cx| {
this.filter_state = FilterState::All; this.filter_on_selected_binding_keystrokes(cx);
this.search_mode = SearchMode::KeyStroke { exact_match: true };
this.keystroke_editor.update(cx, |editor, cx| {
editor.set_keystrokes(key_strokes.clone(), cx);
});
}) })
.ok(); .ok();
} }
@ -826,6 +843,24 @@ impl KeymapEditor {
self.context_menu.is_some() self.context_menu.is_some()
} }
fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement {
let hint = match (self.filter_state, &self.search_mode) {
(FilterState::Conflicts, _) => {
if self.keybinding_conflict_state.any_conflicts() {
"No conflicting keybinds found that match the provided query"
} else {
"No conflicting keybinds found"
}
}
(FilterState::All, SearchMode::KeyStroke { .. }) => {
"No keybinds found matching the entered keystrokes"
}
(FilterState::All, SearchMode::Normal) => "No matches found for the provided query",
};
Label::new(hint).color(Color::Muted).into_any_element()
}
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
self.show_hover_menus = false; self.show_hover_menus = false;
if let Some(selected) = self.selected_index { if let Some(selected) = self.selected_index {
@ -1064,11 +1099,35 @@ impl KeymapEditor {
} }
} }
struct HumanizedActionNameCache {
cache: HashMap<&'static str, SharedString>,
}
impl HumanizedActionNameCache {
fn new(cx: &App) -> Self {
let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
(
action_name,
command_palette::humanize_action_name(action_name).into(),
)
}));
Self { cache }
}
fn get(&self, action_name: &'static str) -> SharedString {
match self.cache.get(action_name) {
Some(name) => name.clone(),
None => action_name.into(),
}
}
}
#[derive(Clone)] #[derive(Clone)]
struct ProcessedKeybinding { struct ProcessedKeybinding {
keystroke_text: SharedString, keystroke_text: SharedString,
ui_key_binding: Option<ui::KeyBinding>, ui_key_binding: Option<ui::KeyBinding>,
action_name: &'static str, action_name: &'static str,
humanized_action_name: SharedString,
action_arguments: Option<SyntaxHighlightedText>, action_arguments: Option<SyntaxHighlightedText>,
action_docs: Option<&'static str>, action_docs: Option<&'static str>,
action_schema: Option<schemars::Schema>, action_schema: Option<schemars::Schema>,
@ -1079,7 +1138,7 @@ struct ProcessedKeybinding {
impl ProcessedKeybinding { impl ProcessedKeybinding {
fn get_action_mapping(&self) -> ActionMapping { fn get_action_mapping(&self) -> ActionMapping {
ActionMapping { ActionMapping {
keystroke_text: self.keystroke_text.clone(), keystrokes: self.keystrokes().map(Vec::from).unwrap_or_default(),
context: self context: self
.context .context
.as_ref() .as_ref()
@ -1223,38 +1282,39 @@ impl Render for KeymapEditor {
window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx); window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
}), }),
) )
.when(self.keybinding_conflict_state.any_conflicts(), |this| { .child(
this.child( IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
IconButton::new("KeymapEditorConflictIcon", IconName::Warning) .shape(ui::IconButtonShape::Square)
.shape(ui::IconButtonShape::Square) .when(self.keybinding_conflict_state.any_conflicts(), |this| {
.tooltip({ this.indicator(Indicator::dot().color(Color::Warning))
let filter_state = self.filter_state; })
.tooltip({
let filter_state = self.filter_state;
move |window, cx| { move |window, cx| {
Tooltip::for_action( Tooltip::for_action(
match filter_state { match filter_state {
FilterState::All => "Show Conflicts", FilterState::All => "Show Conflicts",
FilterState::Conflicts => "Hide Conflicts", FilterState::Conflicts => "Hide Conflicts",
}, },
&ToggleConflictFilter, &ToggleConflictFilter,
window, window,
cx,
)
}
})
.selected_icon_color(Color::Warning)
.toggle_state(matches!(
self.filter_state,
FilterState::Conflicts
))
.on_click(|_, window, cx| {
window.dispatch_action(
ToggleConflictFilter.boxed_clone(),
cx, cx,
); )
}), }
) })
}), .selected_icon_color(Color::Warning)
.toggle_state(matches!(
self.filter_state,
FilterState::Conflicts
))
.on_click(|_, window, cx| {
window.dispatch_action(
ToggleConflictFilter.boxed_clone(),
cx,
);
}),
),
) )
.when_some( .when_some(
match self.search_mode { match self.search_mode {
@ -1310,13 +1370,17 @@ impl Render for KeymapEditor {
Table::new() Table::new()
.interactable(&self.table_interaction_state) .interactable(&self.table_interaction_state)
.striped() .striped()
.empty_table_callback({
let this = cx.entity();
move |window, cx| this.read(cx).render_no_matches_hint(window, cx)
})
.column_widths([ .column_widths([
rems(2.5), DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))),
rems(16.), DefiniteLength::Fraction(0.25),
rems(16.), DefiniteLength::Fraction(0.20),
rems(16.), DefiniteLength::Fraction(0.14),
rems(32.), DefiniteLength::Fraction(0.45),
rems(8.), DefiniteLength::Fraction(0.08),
]) ])
.header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"])
.uniform_list( .uniform_list(
@ -1393,10 +1457,9 @@ impl Render for KeymapEditor {
.id(("keymap action", index)) .id(("keymap action", index))
.child({ .child({
if action_name != gpui::NoAction.name() { if action_name != gpui::NoAction.name() {
this.humanized_action_names binding
.get(action_name) .humanized_action_name
.cloned() .clone()
.unwrap_or(action_name.into())
.into_any_element() .into_any_element()
} else { } else {
const NULL: SharedString = const NULL: SharedString =
@ -1606,7 +1669,7 @@ impl RenderOnce for SyntaxHighlightedText {
runs.push(text_style.to_run(text.len() - offset)); runs.push(text_style.to_run(text.len() - offset));
} }
return StyledText::new(text).with_runs(runs); StyledText::new(text).with_runs(runs)
} }
} }
@ -1621,8 +1684,8 @@ impl InputError {
Self::Warning(message.into()) Self::Warning(message.into())
} }
fn error(message: impl Into<SharedString>) -> Self { fn error(error: anyhow::Error) -> Self {
Self::Error(message.into()) Self::Error(error.to_string().into())
} }
fn content(&self) -> &SharedString { fn content(&self) -> &SharedString {
@ -1630,10 +1693,6 @@ impl InputError {
InputError::Warning(content) | InputError::Error(content) => content, InputError::Warning(content) | InputError::Error(content) => content,
} }
} }
fn is_warning(&self) -> bool {
matches!(self, InputError::Warning(_))
}
} }
struct KeybindingEditorModal { struct KeybindingEditorModal {
@ -1766,17 +1825,14 @@ impl KeybindingEditorModal {
} }
} }
fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool { fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) {
if self if self
.error .error
.as_ref() .as_ref()
.is_some_and(|old_error| old_error.is_warning() && *old_error == error) .is_none_or(|old_error| *old_error != error)
{ {
false
} else {
self.error = Some(error); self.error = Some(error);
cx.notify(); cx.notify();
true
} }
} }
@ -1818,66 +1874,62 @@ impl KeybindingEditorModal {
Ok(Some(context)) Ok(Some(context))
} }
fn save(&mut self, cx: &mut Context<Self>) { fn save_or_display_error(&mut self, cx: &mut Context<Self>) {
self.save(cx).map_err(|err| self.set_error(err, cx)).ok();
}
fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
let existing_keybind = self.editing_keybind.clone(); let existing_keybind = self.editing_keybind.clone();
let fs = self.fs.clone(); let fs = self.fs.clone();
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
let new_keystrokes = match self.validate_keystrokes(cx) {
Err(err) => {
self.set_error(InputError::error(err.to_string()), cx);
return;
}
Ok(keystrokes) => keystrokes,
};
let new_context = match self.validate_context(cx) { let new_keystrokes = self
Err(err) => { .validate_keystrokes(cx)
self.set_error(InputError::error(err.to_string()), cx); .map_err(InputError::error)?
return; .into_iter()
} .map(remove_key_char)
Ok(context) => context, .collect::<Vec<_>>();
};
let new_action_args = match self.validate_action_arguments(cx) { let new_context = self.validate_context(cx).map_err(InputError::error)?;
Err(input_err) => { let new_action_args = self
self.set_error(InputError::error(input_err.to_string()), cx); .validate_action_arguments(cx)
return; .map_err(InputError::error)?;
}
Ok(input) => input,
};
let action_mapping = ActionMapping { let action_mapping = ActionMapping {
keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(), keystrokes: new_keystrokes,
context: new_context.as_ref().map(Into::into), context: new_context.map(SharedString::from),
}; };
let conflicting_indices = if self.creating { let conflicting_indices = if self.creating {
self.keymap_editor self.keymap_editor
.read(cx) .read(cx)
.keybinding_conflict_state .keybinding_conflict_state
.will_conflict(action_mapping) .will_conflict(&action_mapping)
} else { } else {
self.keymap_editor self.keymap_editor
.read(cx) .read(cx)
.keybinding_conflict_state .keybinding_conflict_state
.conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx) .conflicting_indices_for_mapping(&action_mapping, self.editing_keybind_idx)
}; };
if let Some(conflicting_indices) = conflicting_indices {
let first_conflicting_index = conflicting_indices[0]; conflicting_indices.map(|KeybindConflict {
first_conflict_index,
remaining_conflict_amount,
}|
{
let conflicting_action_name = self let conflicting_action_name = self
.keymap_editor .keymap_editor
.read(cx) .read(cx)
.keybindings .keybindings
.get(first_conflicting_index) .get(first_conflict_index)
.map(|keybind| keybind.action_name); .map(|keybind| keybind.action_name);
let warning_message = match conflicting_action_name { let warning_message = match conflicting_action_name {
Some(name) => { Some(name) => {
let confliction_action_amount = conflicting_indices.len() - 1; if remaining_conflict_amount > 0 {
if confliction_action_amount > 0 {
format!( format!(
"Your keybind would conflict with the \"{}\" action and {} other bindings", "Your keybind would conflict with the \"{}\" action and {} other bindings",
name, confliction_action_amount name, remaining_conflict_amount
) )
} else { } else {
format!("Your keybind would conflict with the \"{}\" action", name) format!("Your keybind would conflict with the \"{}\" action", name)
@ -1886,23 +1938,26 @@ impl KeybindingEditorModal {
None => { None => {
log::info!( log::info!(
"Could not find action in keybindings with index {}", "Could not find action in keybindings with index {}",
first_conflicting_index first_conflict_index
); );
"Your keybind would conflict with other actions".to_string() "Your keybind would conflict with other actions".to_string()
} }
}; };
if self.set_error(InputError::warning(warning_message), cx) { let warning = InputError::warning(warning_message);
return; if self.error.as_ref().is_some_and(|old_error| *old_error == warning) {
Ok(())
} else {
Err(warning)
} }
} }).unwrap_or(Ok(()))?;
let create = self.creating; let create = self.creating;
let status_toast = StatusToast::new( let status_toast = StatusToast::new(
format!( format!(
"Saved edits to the {} action.", "Saved edits to the {} action.",
command_palette::humanize_action_name(&self.editing_keybind.action_name) &self.editing_keybind.humanized_action_name
), ),
cx, cx,
move |this, _cx| { move |this, _cx| {
@ -1924,8 +1979,7 @@ impl KeybindingEditorModal {
if let Err(err) = save_keybinding_update( if let Err(err) = save_keybinding_update(
create, create,
existing_keybind, existing_keybind,
&new_keystrokes, &action_mapping,
new_context.as_deref(),
new_action_args.as_deref(), new_action_args.as_deref(),
&fs, &fs,
tab_size, tab_size,
@ -1933,17 +1987,11 @@ impl KeybindingEditorModal {
.await .await
{ {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_error(InputError::error(err.to_string()), cx); this.set_error(InputError::error(err), cx);
}) })
.log_err(); .log_err();
} else { } else {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let action_mapping = ActionMapping {
keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx)
.into(),
context: new_context.map(SharedString::from),
};
this.keymap_editor.update(cx, |keymap, cx| { this.keymap_editor.update(cx, |keymap, cx| {
keymap.previous_edit = Some(PreviousEdit::Keybinding { keymap.previous_edit = Some(PreviousEdit::Keybinding {
action_mapping, action_mapping,
@ -1960,6 +2008,8 @@ impl KeybindingEditorModal {
} }
}) })
.detach(); .detach();
Ok(())
} }
fn key_context(&self) -> KeyContext { fn key_context(&self) -> KeyContext {
@ -1982,7 +2032,7 @@ impl KeybindingEditorModal {
} }
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
self.save(cx); self.save_or_display_error(cx);
} }
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) { fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
@ -1990,11 +2040,17 @@ impl KeybindingEditorModal {
} }
} }
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
Keystroke {
modifiers,
key,
..Default::default()
}
}
impl Render for KeybindingEditorModal { impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors(); let theme = cx.theme().colors();
let action_name =
command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string();
v_flex() v_flex()
.w(rems(34.)) .w(rems(34.))
@ -2014,7 +2070,9 @@ impl Render for KeybindingEditorModal {
.gap_0p5() .gap_0p5()
.border_b_1() .border_b_1()
.border_color(theme.border_variant) .border_color(theme.border_variant)
.child(Label::new(action_name)) .child(Label::new(
self.editing_keybind.humanized_action_name.clone(),
))
.when_some(self.editing_keybind.action_docs, |this, docs| { .when_some(self.editing_keybind.action_docs, |this, docs| {
this.child( this.child(
Label::new(docs).size(LabelSize::Small).color(Color::Muted), Label::new(docs).size(LabelSize::Small).color(Color::Muted),
@ -2085,7 +2143,7 @@ impl Render for KeybindingEditorModal {
) )
.child(Button::new("save-btn", "Save").on_click(cx.listener( .child(Button::new("save-btn", "Save").on_click(cx.listener(
|this, _event, _window, cx| { |this, _event, _window, cx| {
this.save(cx); this.save_or_display_error(cx);
}, },
))), ))),
), ),
@ -2273,8 +2331,7 @@ async fn load_keybind_context_language(
async fn save_keybinding_update( async fn save_keybinding_update(
create: bool, create: bool,
existing: ProcessedKeybinding, existing: ProcessedKeybinding,
new_keystrokes: &[Keystroke], action_mapping: &ActionMapping,
new_context: Option<&str>,
new_args: Option<&str>, new_args: Option<&str>,
fs: &Arc<dyn Fs>, fs: &Arc<dyn Fs>,
tab_size: usize, tab_size: usize,
@ -2301,8 +2358,8 @@ async fn save_keybinding_update(
}; };
let source = settings::KeybindUpdateTarget { let source = settings::KeybindUpdateTarget {
context: new_context, context: action_mapping.context.as_ref().map(|a| &***a),
keystrokes: new_keystrokes, keystrokes: &action_mapping.keystrokes,
action_name: &existing.action_name, action_name: &existing.action_name,
action_arguments: new_args, action_arguments: new_args,
}; };
@ -2772,7 +2829,7 @@ impl Render for KeystrokeInput {
IconName::PlayFilled IconName::PlayFilled
}; };
return h_flex() h_flex()
.id("keystroke-input") .id("keystroke-input")
.track_focus(&self.outer_focus_handle) .track_focus(&self.outer_focus_handle)
.py_2() .py_2()
@ -2895,7 +2952,7 @@ impl Render for KeystrokeInput {
this.clear_keystrokes(&ClearKeystrokes, window, cx); this.clear_keystrokes(&ClearKeystrokes, window, cx);
})), })),
), ),
); )
} }
} }

View file

@ -40,6 +40,10 @@ impl<const COLS: usize> TableContents<COLS> {
TableContents::UniformList(data) => data.row_count, TableContents::UniformList(data) => data.row_count,
} }
} }
fn is_empty(&self) -> bool {
self.len() == 0
}
} }
pub struct TableInteractionState { pub struct TableInteractionState {
@ -375,6 +379,7 @@ pub struct Table<const COLS: usize = 3> {
interaction_state: Option<WeakEntity<TableInteractionState>>, interaction_state: Option<WeakEntity<TableInteractionState>>,
column_widths: Option<[Length; COLS]>, column_widths: Option<[Length; COLS]>,
map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>, map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>,
empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
} }
impl<const COLS: usize> Table<COLS> { impl<const COLS: usize> Table<COLS> {
@ -388,6 +393,7 @@ impl<const COLS: usize> Table<COLS> {
interaction_state: None, interaction_state: None,
column_widths: None, column_widths: None,
map_row: None, map_row: None,
empty_table_callback: None,
} }
} }
@ -460,6 +466,15 @@ impl<const COLS: usize> Table<COLS> {
self.map_row = Some(Rc::new(callback)); self.map_row = Some(Rc::new(callback));
self self
} }
/// Provide a callback that is invoked when the table is rendered without any rows
pub fn empty_table_callback(
mut self,
callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Self {
self.empty_table_callback = Some(Rc::new(callback));
self
}
} }
fn base_cell_style(width: Option<Length>, cx: &App) -> Div { fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
@ -582,6 +597,7 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
}; };
let width = self.width; let width = self.width;
let no_rows_rendered = self.rows.is_empty();
let table = div() let table = div()
.when_some(width, |this, width| this.w(width)) .when_some(width, |this, width| this.w(width))
@ -662,6 +678,21 @@ impl<const COLS: usize> RenderOnce for Table<COLS> {
}) })
}), }),
) )
.when_some(
no_rows_rendered
.then_some(self.empty_table_callback)
.flatten(),
|this, callback| {
this.child(
h_flex()
.size_full()
.p_3()
.items_start()
.justify_center()
.child(callback(window, cx)),
)
},
)
.when_some( .when_some(
width.and(interaction_state.as_ref()), width.and(interaction_state.as_ref()),
|this, interaction_state| { |this, interaction_state| {