1use crate::{SearchResult, VectorStore};
2use editor::{scroll::autoscroll::Autoscroll, Editor};
3use gpui::{
4 actions, elements::*, AnyElement, AppContext, ModelHandle, MouseState, Task, ViewContext,
5 WeakViewHandle,
6};
7use picker::{Picker, PickerDelegate, PickerEvent};
8use project::{Project, ProjectPath};
9use std::{path::Path, sync::Arc, time::Duration};
10use util::ResultExt;
11use workspace::Workspace;
12
13const MIN_QUERY_LEN: usize = 5;
14const EMBEDDING_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(500);
15
16actions!(semantic_search, [Toggle]);
17
18pub type SemanticSearch = Picker<SemanticSearchDelegate>;
19
20pub struct SemanticSearchDelegate {
21 workspace: WeakViewHandle<Workspace>,
22 project: ModelHandle<Project>,
23 vector_store: ModelHandle<VectorStore>,
24 selected_match_index: usize,
25 matches: Vec<SearchResult>,
26}
27
28impl SemanticSearchDelegate {
29 // This is currently searching on every keystroke,
30 // This is wildly overkill, and has the potential to get expensive
31 // We will need to update this to throttle searching
32 pub fn new(
33 workspace: WeakViewHandle<Workspace>,
34 project: ModelHandle<Project>,
35 vector_store: ModelHandle<VectorStore>,
36 ) -> Self {
37 Self {
38 workspace,
39 project,
40 vector_store,
41 selected_match_index: 0,
42 matches: vec![],
43 }
44 }
45}
46
47impl PickerDelegate for SemanticSearchDelegate {
48 fn placeholder_text(&self) -> Arc<str> {
49 "Search repository in natural language...".into()
50 }
51
52 fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
53 if let Some(search_result) = self.matches.get(self.selected_match_index) {
54 // Open Buffer
55 let search_result = search_result.clone();
56 let buffer = self.project.update(cx, |project, cx| {
57 project.open_buffer(
58 ProjectPath {
59 worktree_id: search_result.worktree_id,
60 path: search_result.file_path.clone().into(),
61 },
62 cx,
63 )
64 });
65
66 let workspace = self.workspace.clone();
67 let position = search_result.clone().offset;
68 cx.spawn(|_, mut cx| async move {
69 let buffer = buffer.await?;
70 workspace.update(&mut cx, |workspace, cx| {
71 let editor = workspace.open_project_item::<Editor>(buffer, cx);
72 editor.update(cx, |editor, cx| {
73 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
74 s.select_ranges([position..position])
75 });
76 });
77 })?;
78 Ok::<_, anyhow::Error>(())
79 })
80 .detach_and_log_err(cx);
81 cx.emit(PickerEvent::Dismiss);
82 }
83 }
84
85 fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
86
87 fn match_count(&self) -> usize {
88 self.matches.len()
89 }
90
91 fn selected_index(&self) -> usize {
92 self.selected_match_index
93 }
94
95 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
96 self.selected_match_index = ix;
97 }
98
99 fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
100 if query.len() < MIN_QUERY_LEN {
101 return Task::ready(());
102 }
103
104 let vector_store = self.vector_store.clone();
105 let project = self.project.clone();
106 cx.spawn(|this, mut cx| async move {
107 cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
108
109 log::info!("Searching for {:?}", &query);
110
111 let task = vector_store.update(&mut cx, |store, cx| {
112 store.search(&project, query.to_string(), 10, cx)
113 });
114
115 if let Some(results) = task.await.log_err() {
116 this.update(&mut cx, |this, _| {
117 let delegate = this.delegate_mut();
118 delegate.matches = results;
119 })
120 .ok();
121 }
122 })
123 }
124
125 fn render_match(
126 &self,
127 ix: usize,
128 mouse_state: &mut MouseState,
129 selected: bool,
130 cx: &AppContext,
131 ) -> AnyElement<Picker<Self>> {
132 let theme = theme::current(cx);
133 let style = &theme.picker.item;
134 let current_style = style.in_state(selected).style_for(mouse_state);
135
136 let search_result = &self.matches[ix];
137
138 let mut path = search_result.file_path.to_string_lossy();
139 let name = search_result.name.clone();
140
141 Flex::column()
142 .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
143 .with_child(Label::new(
144 path.to_string(),
145 style.inactive_state().default.label.clone(),
146 ))
147 .contained()
148 .with_style(current_style.container)
149 .into_any()
150 }
151}