diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 9d745347d0..e2770c0148 100644 --- a/crates/gpui/src/elements/component.rs +++ b/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 Component for ElementAdapter { pub struct ComponentAdapter { component: Option, element: Option>, - #[cfg(debug_assertions)] - _component_name: &'static str, + phantom: PhantomData, } impl ComponentAdapter { @@ -115,8 +116,7 @@ impl ComponentAdapter { Self { component: Some(e), element: None, - #[cfg(debug_assertions)] - _component_name: std::any::type_name::(), + phantom: PhantomData, } } } @@ -133,8 +133,12 @@ impl + 'static> Element for ComponentAdapter { cx: &mut LayoutContext, ) -> (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 + 'static> Element for ComponentAdapter { ) -> 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 + 'static> Element for ComponentAdapter { ) -> Option { 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 + 'static> Element for ComponentAdapter { view: &V, cx: &ViewContext, ) -> 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)), }) } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b65c7222a4..4078cb572d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -523,6 +523,11 @@ impl BufferSearchBar { } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + 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.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) { let mut should_propagate = true; diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 0163528951..2c180be761 100644 --- a/crates/search/src/mode.rs +++ b/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 { 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 { 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, } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2cec9610f1..dada928d6e 100644 --- a/crates/search/src/project_search.rs +++ b/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::(SearchOptions::CASE_SENSITIVE, cx); @@ -114,6 +118,8 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, + semantic_state: Option, + semantic_permissioned: Option, search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, @@ -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>, subscription: Option, @@ -206,6 +218,60 @@ impl ProjectSearch { })); cx.notify(); } + + fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { + 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) { + 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.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::>() + .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) -> Task> { + 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) { - 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::()) { 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) { - // if let Some(search_view) = pane - // .active_item() - // .and_then(|item| item.downcast::()) - // { - // 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) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + 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) -> 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| { 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| { 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), diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 67cb876c32..8d8c02c8d7 100644 --- a/crates/search/src/search.rs +++ b/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() diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 5287c999e8..8c3587d942 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case, non_upper_case_globals)] - mod keymap_file; mod settings_file; mod settings_store; diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index a74b9ed4a4..fce7ad825c 100644 --- a/crates/theme/src/components.rs +++ b/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)