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::{collections::HashMap, 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 history: HashMap<String, Vec<SearchResult>>,
27}
28
29impl SemanticSearchDelegate {
30 // This is currently searching on every keystroke,
31 // This is wildly overkill, and has the potential to get expensive
32 // We will need to update this to throttle searching
33 pub fn new(
34 workspace: WeakViewHandle<Workspace>,
35 project: ModelHandle<Project>,
36 vector_store: ModelHandle<VectorStore>,
37 ) -> Self {
38 Self {
39 workspace,
40 project,
41 vector_store,
42 selected_match_index: 0,
43 matches: vec![],
44 history: HashMap::new(),
45 }
46 }
47}
48
49impl PickerDelegate for SemanticSearchDelegate {
50 fn placeholder_text(&self) -> Arc<str> {
51 "Search repository in natural language...".into()
52 }
53
54 fn confirm(&mut self, cx: &mut ViewContext<SemanticSearch>) {
55 if let Some(search_result) = self.matches.get(self.selected_match_index) {
56 // Open Buffer
57 let search_result = search_result.clone();
58 let buffer = self.project.update(cx, |project, cx| {
59 project.open_buffer(
60 ProjectPath {
61 worktree_id: search_result.worktree_id,
62 path: search_result.file_path.clone().into(),
63 },
64 cx,
65 )
66 });
67
68 let workspace = self.workspace.clone();
69 let position = search_result.clone().offset;
70 cx.spawn(|_, mut cx| async move {
71 let buffer = buffer.await?;
72 workspace.update(&mut cx, |workspace, cx| {
73 let editor = workspace.open_project_item::<Editor>(buffer, cx);
74 editor.update(cx, |editor, cx| {
75 editor.change_selections(Some(Autoscroll::center()), cx, |s| {
76 s.select_ranges([position..position])
77 });
78 });
79 })?;
80 Ok::<_, anyhow::Error>(())
81 })
82 .detach_and_log_err(cx);
83 cx.emit(PickerEvent::Dismiss);
84 }
85 }
86
87 fn dismissed(&mut self, _cx: &mut ViewContext<SemanticSearch>) {}
88
89 fn match_count(&self) -> usize {
90 self.matches.len()
91 }
92
93 fn selected_index(&self) -> usize {
94 self.selected_match_index
95 }
96
97 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<SemanticSearch>) {
98 self.selected_match_index = ix;
99 }
100
101 fn update_matches(&mut self, query: String, cx: &mut ViewContext<SemanticSearch>) -> Task<()> {
102 log::info!("Searching for {:?}...", query);
103 if query.len() < MIN_QUERY_LEN {
104 log::info!("Query below minimum length");
105 return Task::ready(());
106 }
107
108 let vector_store = self.vector_store.clone();
109 let project = self.project.clone();
110 cx.spawn(|this, mut cx| async move {
111 cx.background().timer(EMBEDDING_DEBOUNCE_INTERVAL).await;
112
113 let retrieved_cached = this.update(&mut cx, |this, _| {
114 let delegate = this.delegate_mut();
115 if delegate.history.contains_key(&query) {
116 let historic_results = delegate.history.get(&query).unwrap().to_owned();
117 delegate.matches = historic_results.clone();
118 true
119 } else {
120 false
121 }
122 });
123
124 if let Some(retrieved) = retrieved_cached.log_err() {
125 if !retrieved {
126 let task = vector_store.update(&mut cx, |store, cx| {
127 store.search(project.clone(), query.to_string(), 10, cx)
128 });
129
130 if let Some(results) = task.await.log_err() {
131 log::info!("Not queried previously, searching...");
132 this.update(&mut cx, |this, _| {
133 let delegate = this.delegate_mut();
134 delegate.matches = results.clone();
135 delegate.history.insert(query, results);
136 })
137 .ok();
138 }
139 } else {
140 log::info!("Already queried, retrieved directly from cached history");
141 }
142 }
143 })
144 }
145
146 fn render_match(
147 &self,
148 ix: usize,
149 mouse_state: &mut MouseState,
150 selected: bool,
151 cx: &AppContext,
152 ) -> AnyElement<Picker<Self>> {
153 let theme = theme::current(cx);
154 let style = &theme.picker.item;
155 let current_style = style.in_state(selected).style_for(mouse_state);
156
157 let search_result = &self.matches[ix];
158
159 let path = search_result.file_path.to_string_lossy();
160 let name = search_result.name.clone();
161
162 Flex::column()
163 .with_child(Text::new(name, current_style.label.text.clone()).with_soft_wrap(false))
164 .with_child(Label::new(
165 path.to_string(),
166 style.inactive_state().default.label.clone(),
167 ))
168 .contained()
169 .with_style(current_style.container)
170 .into_any()
171 }
172}