Revert "Remove semantic search UI" (#2865)

Mikayla Maki created

This reverts commit c0f042b39a595da6f93de2268e5990a3c5604912, where I
deleted the semantic-search related UI code.

Apologies to @KCaverly for the misunderstanding

Release Notes:

- N/A

Change summary

crates/gpui/src/elements/component.rs |  30 +-
crates/search/src/buffer_search.rs    |   7 
crates/search/src/mode.rs             |  20 +
crates/search/src/project_search.rs   | 304 +++++++++++++++++++++++++---
crates/search/src/search.rs           |   7 
crates/settings/src/settings.rs       |   2 
crates/theme/src/components.rs        |   9 
7 files changed, 305 insertions(+), 74 deletions(-)

Detailed changes

crates/gpui/src/elements/component.rs 🔗

@@ -1,3 +1,5 @@
+use std::marker::PhantomData;
+
 use pathfinder_geometry::{rect::RectF, vector::Vector2F};
 
 use crate::{
@@ -106,8 +108,7 @@ impl<V: View> Component<V> for ElementAdapter<V> {
 pub struct ComponentAdapter<V: View, E> {
     component: Option<E>,
     element: Option<AnyElement<V>>,
-    #[cfg(debug_assertions)]
-    _component_name: &'static str,
+    phantom: PhantomData<V>,
 }
 
 impl<E, V: View> ComponentAdapter<V, E> {
@@ -115,8 +116,7 @@ impl<E, V: View> ComponentAdapter<V, E> {
         Self {
             component: Some(e),
             element: None,
-            #[cfg(debug_assertions)]
-            _component_name: std::any::type_name::<E>(),
+            phantom: PhantomData,
         }
     }
 }
@@ -133,8 +133,12 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
         cx: &mut LayoutContext<V>,
     ) -> (Vector2F, Self::LayoutState) {
         if self.element.is_none() {
-            let component = self.component.take().unwrap();
-            self.element = Some(component.render(view, cx.view_context()));
+            let element = self
+                .component
+                .take()
+                .expect("Component can only be rendered once")
+                .render(view, cx.view_context());
+            self.element = Some(element);
         }
         let constraint = self.element.as_mut().unwrap().layout(constraint, view, cx);
         (constraint, ())
@@ -151,7 +155,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
     ) -> Self::PaintState {
         self.element
             .as_mut()
-            .unwrap()
+            .expect("Layout should always be called before paint")
             .paint(scene, bounds.origin(), visible_bounds, view, cx)
     }
 
@@ -167,8 +171,7 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
     ) -> Option<RectF> {
         self.element
             .as_ref()
-            .unwrap()
-            .rect_for_text_range(range_utf16, view, cx)
+            .and_then(|el| el.rect_for_text_range(range_utf16, view, cx))
     }
 
     fn debug(
@@ -179,16 +182,9 @@ impl<V: View, C: Component<V> + 'static> Element<V> for ComponentAdapter<V, C> {
         view: &V,
         cx: &ViewContext<V>,
     ) -> serde_json::Value {
-        #[cfg(debug_assertions)]
-        let component_name = self._component_name;
-
-        #[cfg(not(debug_assertions))]
-        let component_name = "Unknown";
-
         serde_json::json!({
             "type": "ComponentAdapter",
-            "child": self.element.as_ref().unwrap().debug(view, cx),
-            "component_name": component_name
+            "child": self.element.as_ref().map(|el| el.debug(view, cx)),
         })
     }
 }

crates/search/src/buffer_search.rs 🔗

@@ -523,6 +523,11 @@ impl BufferSearchBar {
     }
 
     pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+        assert_ne!(
+            mode,
+            SearchMode::Semantic,
+            "Semantic search is not supported in buffer search"
+        );
         if mode == self.current_mode {
             return;
         }
@@ -797,7 +802,7 @@ impl BufferSearchBar {
         }
     }
     fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
-        self.activate_search_mode(next_mode(&self.current_mode), cx);
+        self.activate_search_mode(next_mode(&self.current_mode, false), cx);
     }
     fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext<Pane>) {
         let mut should_propagate = true;

crates/search/src/mode.rs 🔗

@@ -1,11 +1,12 @@
 use gpui::Action;
 
-use crate::{ActivateRegexMode, ActivateTextMode};
+use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
 // TODO: Update the default search mode to get from config
 #[derive(Copy, Clone, Debug, Default, PartialEq)]
 pub enum SearchMode {
     #[default]
     Text,
+    Semantic,
     Regex,
 }
 
@@ -19,6 +20,7 @@ impl SearchMode {
     pub(crate) fn label(&self) -> &'static str {
         match self {
             SearchMode::Text => "Text",
+            SearchMode::Semantic => "Semantic",
             SearchMode::Regex => "Regex",
         }
     }
@@ -26,6 +28,7 @@ impl SearchMode {
     pub(crate) fn region_id(&self) -> usize {
         match self {
             SearchMode::Text => 3,
+            SearchMode::Semantic => 4,
             SearchMode::Regex => 5,
         }
     }
@@ -33,6 +36,7 @@ impl SearchMode {
     pub(crate) fn tooltip_text(&self) -> &'static str {
         match self {
             SearchMode::Text => "Activate Text Search",
+            SearchMode::Semantic => "Activate Semantic Search",
             SearchMode::Regex => "Activate Regex Search",
         }
     }
@@ -40,6 +44,7 @@ impl SearchMode {
     pub(crate) fn activate_action(&self) -> Box<dyn Action> {
         match self {
             SearchMode::Text => Box::new(ActivateTextMode),
+            SearchMode::Semantic => Box::new(ActivateSemanticMode),
             SearchMode::Regex => Box::new(ActivateRegexMode),
         }
     }
@@ -48,6 +53,7 @@ impl SearchMode {
         match self {
             SearchMode::Regex => true,
             SearchMode::Text => true,
+            SearchMode::Semantic => true,
         }
     }
 
@@ -61,14 +67,22 @@ impl SearchMode {
     pub(crate) fn button_side(&self) -> Option<Side> {
         match self {
             SearchMode::Text => Some(Side::Left),
+            SearchMode::Semantic => None,
             SearchMode::Regex => Some(Side::Right),
         }
     }
 }
 
-pub(crate) fn next_mode(mode: &SearchMode) -> SearchMode {
+pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
+    let next_text_state = if semantic_enabled {
+        SearchMode::Semantic
+    } else {
+        SearchMode::Regex
+    };
+
     match mode {
-        SearchMode::Text => SearchMode::Regex,
+        SearchMode::Text => next_text_state,
+        SearchMode::Semantic => SearchMode::Regex,
         SearchMode::Regex => SearchMode::Text,
     }
 }

crates/search/src/project_search.rs 🔗

@@ -2,10 +2,10 @@ use crate::{
     history::SearchHistory,
     mode::SearchMode,
     search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
-    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch,
-    SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
+    ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions,
+    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
 };
-use anyhow::Context;
+use anyhow::{Context, Result};
 use collections::HashMap;
 use editor::{
     items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
@@ -13,6 +13,8 @@ use editor::{
 };
 use futures::StreamExt;
 
+use gpui::platform::PromptLevel;
+
 use gpui::{
     actions, elements::*, platform::MouseButton, Action, AnyElement, AnyViewHandle, AppContext,
     Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
@@ -20,10 +22,12 @@ use gpui::{
 };
 
 use menu::Confirm;
+use postage::stream::Stream;
 use project::{
-    search::{PathMatcher, SearchQuery},
+    search::{PathMatcher, SearchInputs, SearchQuery},
     Entry, Project,
 };
+use semantic_index::SemanticIndex;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -60,7 +64,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::cycle_mode);
     cx.add_action(ProjectSearchBar::next_history_query);
     cx.add_action(ProjectSearchBar::previous_history_query);
-    // cx.add_action(ProjectSearchBar::activate_regex_mode);
+    cx.add_action(ProjectSearchBar::activate_regex_mode);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
@@ -114,6 +118,8 @@ pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
+    semantic_state: Option<SemanticSearchState>,
+    semantic_permissioned: Option<bool>,
     search_options: SearchOptions,
     panels_with_errors: HashSet<InputPanel>,
     active_match_index: Option<usize>,
@@ -125,6 +131,12 @@ pub struct ProjectSearchView {
     current_mode: SearchMode,
 }
 
+struct SemanticSearchState {
+    file_count: usize,
+    outstanding_file_count: usize,
+    _progress_task: Task<()>,
+}
+
 pub struct ProjectSearchBar {
     active_project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -206,6 +218,60 @@ impl ProjectSearch {
         }));
         cx.notify();
     }
+
+    fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
+        let search = SemanticIndex::global(cx).map(|index| {
+            index.update(cx, |semantic_index, cx| {
+                semantic_index.search_project(
+                    self.project.clone(),
+                    inputs.as_str().to_owned(),
+                    10,
+                    inputs.files_to_include().to_vec(),
+                    inputs.files_to_exclude().to_vec(),
+                    cx,
+                )
+            })
+        });
+        self.search_id += 1;
+        self.match_ranges.clear();
+        self.search_history.add(inputs.as_str().to_string());
+        self.no_results = Some(true);
+        self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+            let results = search?.await.log_err()?;
+
+            let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
+                this.excerpts.update(cx, |excerpts, cx| {
+                    excerpts.clear(cx);
+
+                    let matches = results
+                        .into_iter()
+                        .map(|result| (result.buffer, vec![result.range.start..result.range.start]))
+                        .collect();
+
+                    excerpts.stream_excerpts_with_context_lines(matches, 3, cx)
+                })
+            });
+
+            while let Some(match_range) = match_ranges.next().await {
+                this.update(&mut cx, |this, cx| {
+                    this.match_ranges.push(match_range);
+                    while let Ok(Some(match_range)) = match_ranges.try_next() {
+                        this.match_ranges.push(match_range);
+                    }
+                    this.no_results = Some(false);
+                    cx.notify();
+                });
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.pending_search.take();
+                cx.notify();
+            });
+
+            None
+        }));
+        cx.notify();
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -245,10 +311,27 @@ impl View for ProjectSearchView {
             } else {
                 match current_mode {
                     SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
+                    SearchMode::Semantic => {
+                        Cow::Borrowed("Search all code objects using Natural Language")
+                    }
                     SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
                 }
             };
 
+            let semantic_status = if let Some(semantic) = &self.semantic_state {
+                if semantic.outstanding_file_count > 0 {
+                    format!(
+                        "Indexing: {} of {}...",
+                        semantic.file_count - semantic.outstanding_file_count,
+                        semantic.file_count
+                    )
+                } else {
+                    "Indexing complete".to_string()
+                }
+            } else {
+                "Indexing: ...".to_string()
+            };
+
             let minor_text = if let Some(no_results) = model.no_results {
                 if model.pending_search.is_none() && no_results {
                     vec!["No results found in this project for the provided query".to_owned()]
@@ -256,11 +339,19 @@ impl View for ProjectSearchView {
                     vec![]
                 }
             } else {
-                vec![
-                    "".to_owned(),
-                    "Include/exclude specific paths with the filter option.".to_owned(),
-                    "Matching exact word and/or casing is available too.".to_owned(),
-                ]
+                match current_mode {
+                    SearchMode::Semantic => vec![
+                        "".to_owned(),
+                        semantic_status,
+                        "Simply explain the code you are looking to find.".to_owned(),
+                        "ex. 'prompt user for permissions to index their project'".to_owned(),
+                    ],
+                    _ => vec![
+                        "".to_owned(),
+                        "Include/exclude specific paths with the filter option.".to_owned(),
+                        "Matching exact word and/or casing is available too.".to_owned(),
+                    ],
+                }
             };
 
             let previous_query_keystrokes =
@@ -539,6 +630,49 @@ impl ProjectSearchView {
         self.search_options.toggle(option);
     }
 
+    fn index_project(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(semantic_index) = SemanticIndex::global(cx) {
+            // Semantic search uses no options
+            self.search_options = SearchOptions::none();
+
+            let project = self.model.read(cx).project.clone();
+            let index_task = semantic_index.update(cx, |semantic_index, cx| {
+                semantic_index.index_project(project, cx)
+            });
+
+            cx.spawn(|search_view, mut cx| async move {
+                let (files_to_index, mut files_remaining_rx) = index_task.await?;
+
+                search_view.update(&mut cx, |search_view, cx| {
+                    cx.notify();
+                    search_view.semantic_state = Some(SemanticSearchState {
+                        file_count: files_to_index,
+                        outstanding_file_count: files_to_index,
+                        _progress_task: cx.spawn(|search_view, mut cx| async move {
+                            while let Some(count) = files_remaining_rx.recv().await {
+                                search_view
+                                    .update(&mut cx, |search_view, cx| {
+                                        if let Some(semantic_search_state) =
+                                            &mut search_view.semantic_state
+                                        {
+                                            semantic_search_state.outstanding_file_count = count;
+                                            cx.notify();
+                                            if count == 0 {
+                                                return;
+                                            }
+                                        }
+                                    })
+                                    .ok();
+                            }
+                        }),
+                    });
+                })?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
     fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
         self.model.update(cx, |model, cx| {
             model.pending_search = None;
@@ -561,7 +695,61 @@ impl ProjectSearchView {
         self.current_mode = mode;
         self.active_match_index = None;
 
-        self.search(cx);
+        match mode {
+            SearchMode::Semantic => {
+                let has_permission = self.semantic_permissioned(cx);
+                self.active_match_index = None;
+                cx.spawn(|this, mut cx| async move {
+                    let has_permission = has_permission.await?;
+
+                    if !has_permission {
+                        let mut answer = this.update(&mut cx, |this, cx| {
+                            let project = this.model.read(cx).project.clone();
+                            let project_name = project
+                                .read(cx)
+                                .worktree_root_names(cx)
+                                .collect::<Vec<&str>>()
+                                .join("/");
+                            let is_plural =
+                                project_name.chars().filter(|letter| *letter == '/').count() > 0;
+                            let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
+                                if is_plural {
+                                    "s"
+                                } else {""});
+                            cx.prompt(
+                                PromptLevel::Info,
+                                prompt_text.as_str(),
+                                &["Continue", "Cancel"],
+                            )
+                        })?;
+
+                        if answer.next().await == Some(0) {
+                            this.update(&mut cx, |this, _| {
+                                this.semantic_permissioned = Some(true);
+                            })?;
+                        } else {
+                            this.update(&mut cx, |this, cx| {
+                                this.semantic_permissioned = Some(false);
+                                debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected");
+                                this.activate_search_mode(previous_mode, cx);
+                            })?;
+                            return anyhow::Ok(());
+                        }
+                    }
+
+                    this.update(&mut cx, |this, cx| {
+                        this.index_project(cx);
+                    })?;
+
+                    anyhow::Ok(())
+                }).detach_and_log_err(cx);
+            }
+            SearchMode::Regex | SearchMode::Text => {
+                self.semantic_state = None;
+                self.active_match_index = None;
+                self.search(cx);
+            }
+        }
 
         cx.notify();
     }
@@ -657,6 +845,8 @@ impl ProjectSearchView {
             model,
             query_editor,
             results_editor,
+            semantic_state: None,
+            semantic_permissioned: None,
             search_options: options,
             panels_with_errors: HashSet::new(),
             active_match_index: None,
@@ -670,6 +860,18 @@ impl ProjectSearchView {
         this
     }
 
+    fn semantic_permissioned(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
+        if let Some(value) = self.semantic_permissioned {
+            return Task::ready(Ok(value));
+        }
+
+        SemanticIndex::global(cx)
+            .map(|semantic| {
+                let project = self.model.read(cx).project.clone();
+                semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx))
+            })
+            .unwrap_or(Task::ready(Ok(false)))
+    }
     pub fn new_search_in_directory(
         workspace: &mut Workspace,
         dir_entry: &Entry,
@@ -745,8 +947,26 @@ impl ProjectSearchView {
     }
 
     fn search(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(query) = self.build_search_query(cx) {
-            self.model.update(cx, |model, cx| model.search(query, cx));
+        let mode = self.current_mode;
+        match mode {
+            SearchMode::Semantic => {
+                if let Some(semantic) = &mut self.semantic_state {
+                    if semantic.outstanding_file_count > 0 {
+                        return;
+                    }
+
+                    if let Some(query) = self.build_search_query(cx) {
+                        self.model
+                            .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
+                    }
+                }
+            }
+
+            _ => {
+                if let Some(query) = self.build_search_query(cx) {
+                    self.model.update(cx, |model, cx| model.search(query, cx));
+                }
+            }
         }
     }
 
@@ -946,7 +1166,8 @@ impl ProjectSearchBar {
             .and_then(|item| item.downcast::<ProjectSearchView>())
         {
             search_view.update(cx, |this, cx| {
-                let new_mode = crate::mode::next_mode(&this.current_mode);
+                let new_mode =
+                    crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx));
                 this.activate_search_mode(new_mode, cx);
                 cx.focus(&this.query_editor);
             })
@@ -1071,18 +1292,18 @@ impl ProjectSearchBar {
         }
     }
 
-    // fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
-    //     if let Some(search_view) = pane
-    //         .active_item()
-    //         .and_then(|item| item.downcast::<ProjectSearchView>())
-    //     {
-    //         search_view.update(cx, |view, cx| {
-    //             view.activate_search_mode(SearchMode::Regex, cx)
-    //         });
-    //     } else {
-    //         cx.propagate_action();
-    //     }
-    // }
+    fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| {
+                view.activate_search_mode(SearchMode::Regex, cx)
+            });
+        } else {
+            cx.propagate_action();
+        }
+    }
 
     fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
@@ -1195,7 +1416,8 @@ impl View for ProjectSearchBar {
                 },
                 cx,
             );
-
+            let search = _search.read(cx);
+            let is_semantic_disabled = search.semantic_state.is_none();
             let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
                 crate::search_bar::render_option_button_icon(
                     self.is_option_enabled(option, cx),
@@ -1209,17 +1431,17 @@ impl View for ProjectSearchBar {
                     cx,
                 )
             };
-            let case_sensitive = render_option_button_icon(
-                "icons/case_insensitive_12.svg",
-                SearchOptions::CASE_SENSITIVE,
-                cx,
-            );
+            let case_sensitive = is_semantic_disabled.then(|| {
+                render_option_button_icon(
+                    "icons/case_insensitive_12.svg",
+                    SearchOptions::CASE_SENSITIVE,
+                    cx,
+                )
+            });
 
-            let whole_word = render_option_button_icon(
-                "icons/word_search_12.svg",
-                SearchOptions::WHOLE_WORD,
-                cx,
-            );
+            let whole_word = is_semantic_disabled.then(|| {
+                render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
+            });
 
             let search = _search.read(cx);
             let icon_style = theme.search.editor_icon.clone();
@@ -1235,8 +1457,8 @@ impl View for ProjectSearchBar {
                 .with_child(
                     Flex::row()
                         .with_child(filter_button)
-                        .with_child(case_sensitive)
-                        .with_child(whole_word)
+                        .with_children(case_sensitive)
+                        .with_children(whole_word)
                         .flex(1., false)
                         .constrained()
                         .contained(),
@@ -1335,7 +1557,8 @@ impl View for ProjectSearchBar {
                 )
             };
             let is_active = search.active_match_index.is_some();
-
+            let semantic_index = SemanticIndex::enabled(cx)
+                .then(|| search_button_for_mode(SearchMode::Semantic, cx));
             let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
                 render_nav_button(
                     label,
@@ -1361,6 +1584,7 @@ impl View for ProjectSearchBar {
                 .with_child(
                     Flex::row()
                         .with_child(search_button_for_mode(SearchMode::Text, cx))
+                        .with_children(semantic_index)
                         .with_child(search_button_for_mode(SearchMode::Regex, cx))
                         .contained()
                         .with_style(theme.search.modes_container),

crates/search/src/search.rs 🔗

@@ -8,9 +8,7 @@ use gpui::{
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{
-    action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle,
-};
+use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle};
 
 pub mod buffer_search;
 mod history;
@@ -35,6 +33,7 @@ actions!(
         NextHistoryQuery,
         PreviousHistoryQuery,
         ActivateTextMode,
+        ActivateSemanticMode,
         ActivateRegexMode
     ]
 );
@@ -95,7 +94,7 @@ impl SearchOptions {
             format!("Toggle {}", self.label()),
             tooltip_style,
         )
-        .with_contents(Svg::new(self.icon()))
+        .with_contents(theme::components::svg::Svg::new(self.icon()))
         .toggleable(active)
         .with_style(button_style)
         .element()

crates/theme/src/components.rs 🔗

@@ -175,13 +175,8 @@ pub mod action_button {
             .on_click(MouseButton::Left, {
                 let action = self.action.boxed_clone();
                 move |_, _, cx| {
-                    let window = cx.window();
-                    let view = cx.view_id();
-                    let action = action.boxed_clone();
-                    cx.spawn(|_, mut cx| async move {
-                        window.dispatch_action(view, action.as_ref(), &mut cx);
-                    })
-                    .detach();
+                    cx.window()
+                        .dispatch_action(cx.view_id(), action.as_ref(), cx);
                 }
             })
             .with_cursor_style(CursorStyle::PointingHand)