From 72de2cebda6a380e72fa183e7a5ea6a6c9ad95ad Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 09:57:16 -0500 Subject: [PATCH 1/6] separate action input into separate column --- crates/settings_ui/src/keybindings.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 6021f74a4614d4..991a6a26712214 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -461,8 +461,8 @@ impl Render for KeymapEditor { Table::new() .interactable(&self.table_interaction_state) .striped() - .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)]) - .header(["Command", "Keystrokes", "Context", "Source"]) + .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)]) + .header(["Action", "Arguments", "Keystrokes", "Context", "Source"]) .selected_item_index(self.selected_index) .on_click_row(cx.processor(|this, row_index, _window, _cx| { this.selected_index = Some(row_index); @@ -475,18 +475,13 @@ impl Render for KeymapEditor { .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; - let action = h_flex() - .items_start() - .gap_1() - .child(binding.action.clone()) - .when_some( - binding.action_input.clone(), - |this, binding_input| this.child(binding_input), - ); + let action = binding.action.clone(); let keystrokes = binding.ui_key_binding.clone().map_or( binding.keystroke_text.clone().into_any_element(), IntoElement::into_any_element, ); + let action_input = + binding.action_input.clone().unwrap_or_default(); let context = binding.context.clone(); let source = binding .source @@ -495,6 +490,7 @@ impl Render for KeymapEditor { .unwrap_or_default(); Some([ action.into_any_element(), + action_input.into_any_element(), keystrokes, context.into_any_element(), source.into_any_element(), From b6343e0fa171c3abc0383ecafbf7cd3ecdf95a04 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 10:27:53 -0500 Subject: [PATCH 2/6] AI syntax highlighting impl --- Cargo.lock | 2 + crates/settings_ui/Cargo.toml | 2 + crates/settings_ui/src/keybindings.rs | 92 +++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c523c12b135bcf..5dbadb25b66088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14581,6 +14581,7 @@ dependencies = [ "fs", "fuzzy", "gpui", + "language", "log", "menu", "paths", @@ -14590,6 +14591,7 @@ dependencies = [ "serde", "settings", "theme", + "tree-sitter-json", "ui", "util", "workspace", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 7b01fcc0e6599d..8a4d74aa784fe2 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -22,6 +22,7 @@ feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true +language.workspace = true log.workspace = true menu.workspace = true paths.workspace = true @@ -31,6 +32,7 @@ schemars.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true +tree-sitter-json.workspace = true ui.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 991a6a26712214..130db471bff7eb 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,7 +11,9 @@ use gpui::{ FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, Subscription, WeakEntity, actions, div, }; +use language::{HighlightId, Language, LanguageConfig}; use settings::KeybindSource; + use util::ResultExt; use ui::{ @@ -206,6 +208,15 @@ impl KeymapEditor { let key_bindings = lock.bindings(); let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names()); + // Create JSON language for syntax highlighting + let json_language = Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + ..Default::default() + }, + Some(tree_sitter_json::LANGUAGE.into()), + )); + let mut processed_bindings = Vec::new(); let mut string_match_candidates = Vec::new(); @@ -230,11 +241,19 @@ impl KeymapEditor { let index = processed_bindings.len(); let string_match_candidate = StringMatchCandidate::new(index, &action_name); + + // Calculate JSON syntax highlights for action_input + let action_input = key_binding.action_input(); + let action_input_highlights = action_input + .as_ref() + .map(|input| json_language.highlight_text(&input.as_ref().into(), 0..input.len())); + processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, action: action_name.into(), - action_input: key_binding.action_input(), + action_input, + action_input_highlights, context: context.into(), source, }); @@ -250,6 +269,7 @@ impl KeymapEditor { ui_key_binding: None, action: (*action_name).into(), action_input: None, + action_input_highlights: None, context: empty.clone(), source: None, }); @@ -410,6 +430,7 @@ struct ProcessedKeybinding { ui_key_binding: Option, action: SharedString, action_input: Option, + action_input_highlights: Option, language::HighlightId)>>, context: SharedString, source: Option<(KeybindSource, SharedString)>, } @@ -470,7 +491,8 @@ impl Render for KeymapEditor { .uniform_list( "keymap-editor-table", row_count, - cx.processor(move |this, range: Range, _window, _cx| { + cx.processor(move |this, range: Range, _window, cx| { + let syntax_theme = cx.theme().syntax(); range .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; @@ -480,8 +502,68 @@ impl Render for KeymapEditor { binding.keystroke_text.clone().into_any_element(), IntoElement::into_any_element, ); - let action_input = - binding.action_input.clone().unwrap_or_default(); + + // Clone the data we need to avoid lifetime issues + let action_input = binding.action_input.clone(); + let action_input_highlights = + binding.action_input_highlights.clone(); + + // Render action_input with JSON syntax highlighting + let action_input_element = if let Some(input) = action_input { + if let Some(highlights) = action_input_highlights { + let mut elements = Vec::new(); + let mut last_highlight_end = 0; + + for (highlight_range, highlight_id) in highlights { + // Add un-highlighted text before the current highlight + if highlight_range.start > last_highlight_end { + let substring = input.as_ref() + [last_highlight_end..highlight_range.start] + .to_string(); + elements.push( + div() + .child(SharedString::from(substring)) + .into_any_element(), + ); + } + + // Add the highlighted text + let substring = input.as_ref() + [highlight_range.clone()] + .to_string(); + let mut element = + div().child(SharedString::from(substring)); + if let Some(style) = + highlight_id.style(&syntax_theme) + { + if let Some(color) = style.color { + element = element.text_color(color); + } + } + elements.push(element.into_any_element()); + last_highlight_end = highlight_range.end; + } + + // Add any remaining un-highlighted text + if last_highlight_end < input.len() { + let substring = input.as_ref() + [last_highlight_end..input.len()] + .to_string(); + elements.push( + div() + .child(SharedString::from(substring)) + .into_any_element(), + ); + } + + div().flex().children(elements).into_any_element() + } else { + input.into_any_element() + } + } else { + SharedString::default().into_any_element() + }; + let context = binding.context.clone(); let source = binding .source @@ -490,7 +572,7 @@ impl Render for KeymapEditor { .unwrap_or_default(); Some([ action.into_any_element(), - action_input.into_any_element(), + action_input_element, keystrokes, context.into_any_element(), source.into_any_element(), From ccbc080ef9e789fa4bd4df9de257877e79e72bf4 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 12:14:51 -0500 Subject: [PATCH 3/6] extract JSON rendering into element --- crates/settings_ui/src/keybindings.rs | 162 ++++++++++++-------------- 1 file changed, 76 insertions(+), 86 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 130db471bff7eb..6a769c47a50168 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -238,22 +238,17 @@ impl KeymapEditor { let action_name = key_binding.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 string_match_candidate = StringMatchCandidate::new(index, &action_name); - - // Calculate JSON syntax highlights for action_input - let action_input = key_binding.action_input(); - let action_input_highlights = action_input - .as_ref() - .map(|input| json_language.highlight_text(&input.as_ref().into(), 0..input.len())); - processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, action: action_name.into(), action_input, - action_input_highlights, context: context.into(), source, }); @@ -269,7 +264,6 @@ impl KeymapEditor { ui_key_binding: None, action: (*action_name).into(), action_input: None, - action_input_highlights: None, context: empty.clone(), source: None, }); @@ -429,8 +423,7 @@ struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option, action: SharedString, - action_input: Option, - action_input_highlights: Option, language::HighlightId)>>, + action_input: Option, context: SharedString, source: Option<(KeybindSource, SharedString)>, } @@ -491,92 +484,31 @@ impl Render for KeymapEditor { .uniform_list( "keymap-editor-table", row_count, - cx.processor(move |this, range: Range, _window, cx| { - let syntax_theme = cx.theme().syntax(); + cx.processor(move |this, range: Range, _window, _cx| { range .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; - let action = binding.action.clone(); + + let action = binding.action.clone().into_any_element(); let keystrokes = binding.ui_key_binding.clone().map_or( binding.keystroke_text.clone().into_any_element(), IntoElement::into_any_element, ); - - // Clone the data we need to avoid lifetime issues - let action_input = binding.action_input.clone(); - let action_input_highlights = - binding.action_input_highlights.clone(); - - // Render action_input with JSON syntax highlighting - let action_input_element = if let Some(input) = action_input { - if let Some(highlights) = action_input_highlights { - let mut elements = Vec::new(); - let mut last_highlight_end = 0; - - for (highlight_range, highlight_id) in highlights { - // Add un-highlighted text before the current highlight - if highlight_range.start > last_highlight_end { - let substring = input.as_ref() - [last_highlight_end..highlight_range.start] - .to_string(); - elements.push( - div() - .child(SharedString::from(substring)) - .into_any_element(), - ); - } - - // Add the highlighted text - let substring = input.as_ref() - [highlight_range.clone()] - .to_string(); - let mut element = - div().child(SharedString::from(substring)); - if let Some(style) = - highlight_id.style(&syntax_theme) - { - if let Some(color) = style.color { - element = element.text_color(color); - } - } - elements.push(element.into_any_element()); - last_highlight_end = highlight_range.end; - } - - // Add any remaining un-highlighted text - if last_highlight_end < input.len() { - let substring = input.as_ref() - [last_highlight_end..input.len()] - .to_string(); - elements.push( - div() - .child(SharedString::from(substring)) - .into_any_element(), - ); - } - - div().flex().children(elements).into_any_element() - } else { + let action_input = binding + .action_input + .clone() + .map_or(gpui::Empty.into_any_element(), |input| { input.into_any_element() - } - } else { - SharedString::default().into_any_element() - }; - - let context = binding.context.clone(); + }); + let context = binding.context.clone().into_any_element(); let source = binding .source .clone() .map(|(_source, name)| name) - .unwrap_or_default(); - Some([ - action.into_any_element(), - action_input_element, - keystrokes, - context.into_any_element(), - source.into_any_element(), - ]) + .unwrap_or_default() + .into_any_element(); + Some([action, action_input, keystrokes, context, source]) }) .collect() }), @@ -585,6 +517,58 @@ impl Render for KeymapEditor { } } +#[derive(Debug, Clone, IntoElement)] +struct TextWithSyntaxHighlighting { + text: SharedString, + language: Arc, +} + +impl TextWithSyntaxHighlighting { + pub fn new(text: impl Into, language: Arc) -> 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 { editing_keybind: ProcessedKeybinding, keybind_editor: Entity, @@ -736,7 +720,10 @@ async fn save_keybinding_update( keystrokes: existing_keystrokes, action_name: &existing.action, 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 .source @@ -747,7 +734,10 @@ async fn save_keybinding_update( keystrokes: new_keystrokes, action_name: &existing.action, 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 { From 226b8f778bad33f108e7e64b98a5033112c41fad Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 12:14:51 -0500 Subject: [PATCH 4/6] load JSON language async to get highlights --- crates/settings_ui/src/keybindings.rs | 153 ++++++++++++++++---------- 1 file changed, 96 insertions(+), 57 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 6a769c47a50168..d3b559315a1210 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -7,11 +7,11 @@ use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, Subscription, - WeakEntity, actions, div, + AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, + Subscription, WeakEntity, actions, div, }; -use language::{HighlightId, Language, LanguageConfig}; +use language::{Language, LanguageConfig}; use settings::KeybindSource; use util::ResultExt; @@ -169,38 +169,47 @@ impl KeymapEditor { this } - fn update_matches(&mut self, cx: &mut Context) { - let query = 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(); - let query = command_palette::normalize_action_query(&query); - let fuzzy_match = cx.background_spawn(async move { - fuzzy::match_strings( - &string_match_candidates, - &query, - true, - true, - keybind_count, - &Default::default(), - executor, - ) - .await - }); + fn current_query(&self, cx: &mut Context) -> String { + self.filter_editor.read(cx).text(cx) + } - cx.spawn(async move |this, cx| { - let matches = fuzzy_match.await; - this.update(cx, |this, cx| { - this.selected_index.take(); - this.scroll_to_item(0, ScrollStrategy::Top, cx); - this.matches = matches; - cx.notify(); - }) + fn update_matches(&self, cx: &mut Context) { + 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, + query: String, + cx: &mut AsyncApp, + ) -> Result<(), db::anyhow::Error> { + let query = command_palette::normalize_action_query(&query); + let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| { + (this.string_match_candidates.clone(), this.keybindings.len()) + })?; + let executor = cx.background_executor().clone(); + let matches = fuzzy::match_strings( + &string_match_candidates, + &query, + true, + true, + keybind_count, + &Default::default(), + executor, + ) + .await; + this.update(cx, |this, cx| { + this.selected_index.take(); + this.scroll_to_item(0, ScrollStrategy::Top, cx); + this.matches = matches; + cx.notify(); }) - .detach(); } fn process_bindings( + json_language: Arc, cx: &mut Context, ) -> (Vec, Vec) { let key_bindings_ptr = cx.key_bindings(); @@ -208,15 +217,6 @@ impl KeymapEditor { let key_bindings = lock.bindings(); let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names()); - // Create JSON language for syntax highlighting - let json_language = Arc::new(Language::new( - LanguageConfig { - name: "JSON".into(), - ..Default::default() - }, - Some(tree_sitter_json::LANGUAGE.into()), - )); - let mut processed_bindings = Vec::new(); let mut string_match_candidates = Vec::new(); @@ -273,24 +273,63 @@ impl KeymapEditor { (processed_bindings, string_match_candidates) } - fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context) { - let (key_bindings, string_match_candidates) = Self::process_bindings(cx); - self.keybindings = key_bindings; - self.string_match_candidates = Arc::new(string_match_candidates); - self.matches = self - .string_match_candidates - .iter() - .enumerate() - .map(|(ix, candidate)| StringMatch { - candidate_id: ix, - score: 0.0, - positions: vec![], - string: candidate.string.clone(), - }) - .collect(); + fn update_keybindings(&mut self, cx: &mut Context) { + let workspace = self.workspace.clone(); + cx.spawn(async move |this, cx| { + let json_language = Self::load_json_language(workspace, cx).await; + 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 + .iter() + .enumerate() + .map(|(ix, candidate)| StringMatch { + candidate_id: ix, + score: 0.0, + positions: vec![], + string: candidate.string.clone(), + }) + .collect(); + this.current_query(cx) + })?; + // calls cx.notify + Self::process_query(this, query, cx).await + }) + .detach_and_log_err(cx); + } - self.update_matches(cx); - cx.notify(); + async fn load_json_language( + workspace: WeakEntity, + cx: &mut AsyncApp, + ) -> Arc { + let default = Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + ..Default::default() + }, + Some(tree_sitter_json::LANGUAGE.into()), + )); + let json_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("JSON") + }) + // todo: anyhow context + .log_err(); + let Some(json_language_task) = json_language_task else { + return default; + }; + // todo: anyhow context + let Some(json_language) = json_language_task.await.log_err() else { + return default; + }; + return json_language; } fn dispatch_context(&self, _window: &Window, _cx: &Context) -> KeyContext { From 40ea52d5a88790a6dab7b85978e59eef141f9d7f Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 12:24:49 -0500 Subject: [PATCH 5/6] sort cargo.toml --- crates/settings_ui/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 8a4d74aa784fe2..7dabbb30e0d2f3 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -12,10 +12,10 @@ workspace = true path = "src/settings_ui.rs" [dependencies] +collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true component.workspace = true -collections.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true @@ -27,8 +27,8 @@ log.workspace = true menu.workspace = true paths.workspace = true project.workspace = true -search.workspace = true schemars.workspace = true +search.workspace = true serde.workspace = true settings.workspace = true theme.workspace = true From 45ff762578eb041afdd775d061166007cf104abd Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 1 Jul 2025 12:40:00 -0500 Subject: [PATCH 6/6] add anyhow to settings_ui --- Cargo.lock | 1 + crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 30 +++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dbadb25b66088..afce0f9f1efe1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14571,6 +14571,7 @@ dependencies = [ name = "settings_ui" version = "0.1.0" dependencies = [ + "anyhow", "collections", "command_palette", "command_palette_hooks", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 7dabbb30e0d2f3..6db6d78cd61111 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -12,6 +12,7 @@ workspace = true path = "src/settings_ui.rs" [dependencies] +anyhow.workspace = true collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index d3b559315a1210..73b5d06ba0b0be 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,7 +1,7 @@ use std::{ops::Range, sync::Arc}; +use anyhow::{Context as _, anyhow}; use collections::HashSet; -use db::anyhow::anyhow; use editor::{Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; @@ -305,13 +305,6 @@ impl KeymapEditor { workspace: WeakEntity, cx: &mut AsyncApp, ) -> Arc { - let default = Arc::new(Language::new( - LanguageConfig { - name: "JSON".into(), - ..Default::default() - }, - Some(tree_sitter_json::LANGUAGE.into()), - )); let json_language_task = workspace .read_with(cx, |workspace, cx| { workspace @@ -320,16 +313,21 @@ impl KeymapEditor { .languages() .language_for_name("JSON") }) - // todo: anyhow context + .context("Failed to load JSON language") .log_err(); - let Some(json_language_task) = json_language_task else { - return default; - }; - // todo: anyhow context - let Some(json_language) = json_language_task.await.log_err() else { - return default; + let json_language = match json_language_task { + Some(task) => task.await.context("Failed to load JSON language").log_err(), + None => None, }; - return json_language; + 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) -> KeyContext {