keymap_ui: Separate action input into separate column and highlight as JSON (#33726)
Closes #ISSUE Separates the action input in the Keymap UI into it's own column, and wraps the input in an `impl RenderOnce` element that highlights it as JSON. Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
parent
f1f19a32fb
commit
0eee768e7b
3 changed files with 183 additions and 72 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -14567,6 +14567,7 @@ dependencies = [
|
||||||
name = "settings_ui"
|
name = "settings_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette",
|
"command_palette",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
@ -14577,6 +14578,7 @@ dependencies = [
|
||||||
"fs",
|
"fs",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
"paths",
|
"paths",
|
||||||
|
@ -14586,6 +14588,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"theme",
|
"theme",
|
||||||
|
"tree-sitter-json",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
|
|
@ -12,25 +12,28 @@ workspace = true
|
||||||
path = "src/settings_ui.rs"
|
path = "src/settings_ui.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
command_palette.workspace = true
|
command_palette.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
collections.workspace = true
|
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
search.workspace = true
|
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
search.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
tree-sitter-json.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::{Context as _, anyhow};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use db::anyhow::anyhow;
|
|
||||||
use editor::{Editor, EditorEvent};
|
use editor::{Editor, EditorEvent};
|
||||||
use feature_flags::FeatureFlagViewExt;
|
use feature_flags::FeatureFlagViewExt;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, Subscription,
|
FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText,
|
||||||
WeakEntity, actions, div,
|
Subscription, WeakEntity, actions, div,
|
||||||
};
|
};
|
||||||
|
use language::{Language, LanguageConfig};
|
||||||
use settings::KeybindSource;
|
use settings::KeybindSource;
|
||||||
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
use ui::{
|
use ui::{
|
||||||
|
@ -167,14 +169,28 @@ impl KeymapEditor {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_matches(&mut self, cx: &mut Context<Self>) {
|
fn current_query(&self, cx: &mut Context<Self>) -> String {
|
||||||
let query = self.filter_editor.read(cx).text(cx);
|
self.filter_editor.read(cx).text(cx)
|
||||||
let string_match_candidates = self.string_match_candidates.clone();
|
}
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
let keybind_count = self.keybindings.len();
|
fn update_matches(&self, cx: &mut Context<Self>) {
|
||||||
|
let query = self.current_query(cx);
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_query(
|
||||||
|
this: WeakEntity<Self>,
|
||||||
|
query: String,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<(), db::anyhow::Error> {
|
||||||
let query = command_palette::normalize_action_query(&query);
|
let query = command_palette::normalize_action_query(&query);
|
||||||
let fuzzy_match = cx.background_spawn(async move {
|
let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| {
|
||||||
fuzzy::match_strings(
|
(this.string_match_candidates.clone(), this.keybindings.len())
|
||||||
|
})?;
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
&string_match_candidates,
|
&string_match_candidates,
|
||||||
&query,
|
&query,
|
||||||
true,
|
true,
|
||||||
|
@ -183,22 +199,17 @@ impl KeymapEditor {
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
executor,
|
executor,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let matches = fuzzy_match.await;
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.selected_index.take();
|
this.selected_index.take();
|
||||||
this.scroll_to_item(0, ScrollStrategy::Top, cx);
|
this.scroll_to_item(0, ScrollStrategy::Top, cx);
|
||||||
this.matches = matches;
|
this.matches = matches;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_bindings(
|
fn process_bindings(
|
||||||
|
json_language: Arc<Language>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
|
) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
|
||||||
let key_bindings_ptr = cx.key_bindings();
|
let key_bindings_ptr = cx.key_bindings();
|
||||||
|
@ -227,6 +238,9 @@ impl KeymapEditor {
|
||||||
|
|
||||||
let action_name = key_binding.action().name();
|
let action_name = key_binding.action().name();
|
||||||
unmapped_action_names.remove(&action_name);
|
unmapped_action_names.remove(&action_name);
|
||||||
|
let action_input = key_binding
|
||||||
|
.action_input()
|
||||||
|
.map(|input| TextWithSyntaxHighlighting::new(input, json_language.clone()));
|
||||||
|
|
||||||
let index = processed_bindings.len();
|
let index = processed_bindings.len();
|
||||||
let string_match_candidate = StringMatchCandidate::new(index, &action_name);
|
let string_match_candidate = StringMatchCandidate::new(index, &action_name);
|
||||||
|
@ -234,7 +248,7 @@ impl KeymapEditor {
|
||||||
keystroke_text: keystroke_text.into(),
|
keystroke_text: keystroke_text.into(),
|
||||||
ui_key_binding,
|
ui_key_binding,
|
||||||
action: action_name.into(),
|
action: action_name.into(),
|
||||||
action_input: key_binding.action_input(),
|
action_input,
|
||||||
context: context.into(),
|
context: context.into(),
|
||||||
source,
|
source,
|
||||||
});
|
});
|
||||||
|
@ -259,11 +273,16 @@ impl KeymapEditor {
|
||||||
(processed_bindings, string_match_candidates)
|
(processed_bindings, string_match_candidates)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
|
fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) {
|
||||||
let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
|
let workspace = self.workspace.clone();
|
||||||
self.keybindings = key_bindings;
|
cx.spawn(async move |this, cx| {
|
||||||
self.string_match_candidates = Arc::new(string_match_candidates);
|
let json_language = Self::load_json_language(workspace, cx).await;
|
||||||
self.matches = self
|
let query = this.update(cx, |this, cx| {
|
||||||
|
let (key_bindings, string_match_candidates) =
|
||||||
|
Self::process_bindings(json_language.clone(), cx);
|
||||||
|
this.keybindings = key_bindings;
|
||||||
|
this.string_match_candidates = Arc::new(string_match_candidates);
|
||||||
|
this.matches = this
|
||||||
.string_match_candidates
|
.string_match_candidates
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -274,9 +293,41 @@ impl KeymapEditor {
|
||||||
string: candidate.string.clone(),
|
string: candidate.string.clone(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
this.current_query(cx)
|
||||||
|
})?;
|
||||||
|
// calls cx.notify
|
||||||
|
Self::process_query(this, query, cx).await
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
self.update_matches(cx);
|
async fn load_json_language(
|
||||||
cx.notify();
|
workspace: WeakEntity<Workspace>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Arc<Language> {
|
||||||
|
let json_language_task = workspace
|
||||||
|
.read_with(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.languages()
|
||||||
|
.language_for_name("JSON")
|
||||||
|
})
|
||||||
|
.context("Failed to load JSON language")
|
||||||
|
.log_err();
|
||||||
|
let json_language = match json_language_task {
|
||||||
|
Some(task) => task.await.context("Failed to load JSON language").log_err(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
return json_language.unwrap_or_else(|| {
|
||||||
|
Arc::new(Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "JSON".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_json::LANGUAGE.into()),
|
||||||
|
))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
|
fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
|
||||||
|
@ -409,7 +460,7 @@ struct ProcessedKeybinding {
|
||||||
keystroke_text: SharedString,
|
keystroke_text: SharedString,
|
||||||
ui_key_binding: Option<ui::KeyBinding>,
|
ui_key_binding: Option<ui::KeyBinding>,
|
||||||
action: SharedString,
|
action: SharedString,
|
||||||
action_input: Option<SharedString>,
|
action_input: Option<TextWithSyntaxHighlighting>,
|
||||||
context: SharedString,
|
context: SharedString,
|
||||||
source: Option<(KeybindSource, SharedString)>,
|
source: Option<(KeybindSource, SharedString)>,
|
||||||
}
|
}
|
||||||
|
@ -461,8 +512,8 @@ impl Render for KeymapEditor {
|
||||||
Table::new()
|
Table::new()
|
||||||
.interactable(&self.table_interaction_state)
|
.interactable(&self.table_interaction_state)
|
||||||
.striped()
|
.striped()
|
||||||
.column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
|
.column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)])
|
||||||
.header(["Command", "Keystrokes", "Context", "Source"])
|
.header(["Action", "Arguments", "Keystrokes", "Context", "Source"])
|
||||||
.selected_item_index(self.selected_index)
|
.selected_item_index(self.selected_index)
|
||||||
.on_click_row(cx.processor(|this, row_index, _window, _cx| {
|
.on_click_row(cx.processor(|this, row_index, _window, _cx| {
|
||||||
this.selected_index = Some(row_index);
|
this.selected_index = Some(row_index);
|
||||||
|
@ -475,30 +526,26 @@ impl Render for KeymapEditor {
|
||||||
.filter_map(|index| {
|
.filter_map(|index| {
|
||||||
let candidate_id = this.matches.get(index)?.candidate_id;
|
let candidate_id = this.matches.get(index)?.candidate_id;
|
||||||
let binding = &this.keybindings[candidate_id];
|
let binding = &this.keybindings[candidate_id];
|
||||||
let action = h_flex()
|
|
||||||
.items_start()
|
let action = binding.action.clone().into_any_element();
|
||||||
.gap_1()
|
|
||||||
.child(binding.action.clone())
|
|
||||||
.when_some(
|
|
||||||
binding.action_input.clone(),
|
|
||||||
|this, binding_input| this.child(binding_input),
|
|
||||||
);
|
|
||||||
let keystrokes = binding.ui_key_binding.clone().map_or(
|
let keystrokes = binding.ui_key_binding.clone().map_or(
|
||||||
binding.keystroke_text.clone().into_any_element(),
|
binding.keystroke_text.clone().into_any_element(),
|
||||||
IntoElement::into_any_element,
|
IntoElement::into_any_element,
|
||||||
);
|
);
|
||||||
let context = binding.context.clone();
|
let action_input = binding
|
||||||
|
.action_input
|
||||||
|
.clone()
|
||||||
|
.map_or(gpui::Empty.into_any_element(), |input| {
|
||||||
|
input.into_any_element()
|
||||||
|
});
|
||||||
|
let context = binding.context.clone().into_any_element();
|
||||||
let source = binding
|
let source = binding
|
||||||
.source
|
.source
|
||||||
.clone()
|
.clone()
|
||||||
.map(|(_source, name)| name)
|
.map(|(_source, name)| name)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default()
|
||||||
Some([
|
.into_any_element();
|
||||||
action.into_any_element(),
|
Some([action, action_input, keystrokes, context, source])
|
||||||
keystrokes,
|
|
||||||
context.into_any_element(),
|
|
||||||
source.into_any_element(),
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}),
|
}),
|
||||||
|
@ -507,6 +554,58 @@ impl Render for KeymapEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, IntoElement)]
|
||||||
|
struct TextWithSyntaxHighlighting {
|
||||||
|
text: SharedString,
|
||||||
|
language: Arc<Language>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextWithSyntaxHighlighting {
|
||||||
|
pub fn new(text: impl Into<SharedString>, language: Arc<Language>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
language,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for TextWithSyntaxHighlighting {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let text_style = window.text_style();
|
||||||
|
let syntax_theme = cx.theme().syntax();
|
||||||
|
|
||||||
|
let text = self.text.clone();
|
||||||
|
|
||||||
|
let highlights = self
|
||||||
|
.language
|
||||||
|
.highlight_text(&text.as_ref().into(), 0..text.len());
|
||||||
|
let mut runs = Vec::with_capacity(highlights.len());
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
for (highlight_range, highlight_id) in highlights {
|
||||||
|
// Add un-highlighted text before the current highlight
|
||||||
|
if highlight_range.start > offset {
|
||||||
|
runs.push(text_style.to_run(highlight_range.start - offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut run_style = text_style.clone();
|
||||||
|
if let Some(highlight_style) = highlight_id.style(syntax_theme) {
|
||||||
|
run_style = run_style.highlight(highlight_style);
|
||||||
|
}
|
||||||
|
// add the highlighted range
|
||||||
|
runs.push(run_style.to_run(highlight_range.len()));
|
||||||
|
offset = highlight_range.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining un-highlighted text
|
||||||
|
if offset < text.len() {
|
||||||
|
runs.push(text_style.to_run(text.len() - offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
return StyledText::new(text).with_runs(runs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct KeybindingEditorModal {
|
struct KeybindingEditorModal {
|
||||||
editing_keybind: ProcessedKeybinding,
|
editing_keybind: ProcessedKeybinding,
|
||||||
keybind_editor: Entity<KeybindInput>,
|
keybind_editor: Entity<KeybindInput>,
|
||||||
|
@ -658,7 +757,10 @@ async fn save_keybinding_update(
|
||||||
keystrokes: existing_keystrokes,
|
keystrokes: existing_keystrokes,
|
||||||
action_name: &existing.action,
|
action_name: &existing.action,
|
||||||
use_key_equivalents: false,
|
use_key_equivalents: false,
|
||||||
input: existing.action_input.as_ref().map(|input| input.as_ref()),
|
input: existing
|
||||||
|
.action_input
|
||||||
|
.as_ref()
|
||||||
|
.map(|input| input.text.as_ref()),
|
||||||
},
|
},
|
||||||
target_source: existing
|
target_source: existing
|
||||||
.source
|
.source
|
||||||
|
@ -669,7 +771,10 @@ async fn save_keybinding_update(
|
||||||
keystrokes: new_keystrokes,
|
keystrokes: new_keystrokes,
|
||||||
action_name: &existing.action,
|
action_name: &existing.action,
|
||||||
use_key_equivalents: false,
|
use_key_equivalents: false,
|
||||||
input: existing.action_input.as_ref().map(|input| input.as_ref()),
|
input: existing
|
||||||
|
.action_input
|
||||||
|
.as_ref()
|
||||||
|
.map(|input| input.text.as_ref()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue