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:
parent
c9b9b3194e
commit
ce0de10147
2 changed files with 246 additions and 158 deletions
|
@ -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,10 +1282,12 @@ 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| {
|
||||||
|
this.indicator(Indicator::dot().color(Color::Warning))
|
||||||
|
})
|
||||||
.tooltip({
|
.tooltip({
|
||||||
let filter_state = self.filter_state;
|
let filter_state = self.filter_state;
|
||||||
|
|
||||||
|
@ -1253,8 +1314,7 @@ impl Render for KeymapEditor {
|
||||||
cx,
|
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);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue