1use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
2use anyhow::Result;
3use assistant_slash_command::SlashCommandOutputSection;
4use gpui::{AppContext, Task, WeakView};
5use language::{LineEnding, LspAdapterDelegate};
6use semantic_index::SemanticIndex;
7use std::{
8 fmt::Write,
9 path::PathBuf,
10 sync::{atomic::AtomicBool, Arc},
11};
12use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
13use util::ResultExt;
14use workspace::Workspace;
15
16pub(crate) struct SearchSlashCommand;
17
18impl SlashCommand for SearchSlashCommand {
19 fn name(&self) -> String {
20 "search".into()
21 }
22
23 fn description(&self) -> String {
24 "semantically search files".into()
25 }
26
27 fn tooltip_text(&self) -> String {
28 "search".into()
29 }
30
31 fn requires_argument(&self) -> bool {
32 true
33 }
34
35 fn complete_argument(
36 &self,
37 _query: String,
38 _cancel: Arc<AtomicBool>,
39 _cx: &mut AppContext,
40 ) -> Task<Result<Vec<String>>> {
41 Task::ready(Ok(Vec::new()))
42 }
43
44 fn run(
45 self: Arc<Self>,
46 argument: Option<&str>,
47 workspace: WeakView<Workspace>,
48 _delegate: Arc<dyn LspAdapterDelegate>,
49 cx: &mut WindowContext,
50 ) -> Task<Result<SlashCommandOutput>> {
51 let Some(workspace) = workspace.upgrade() else {
52 return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
53 };
54 let Some(argument) = argument else {
55 return Task::ready(Err(anyhow::anyhow!("missing search query")));
56 };
57 if argument.is_empty() {
58 return Task::ready(Err(anyhow::anyhow!("missing search query")));
59 }
60
61 let project = workspace.read(cx).project().clone();
62 let argument = argument.to_string();
63 let fs = project.read(cx).fs().clone();
64 let project_index =
65 cx.update_global(|index: &mut SemanticIndex, cx| index.project_index(project, cx));
66
67 cx.spawn(|cx| async move {
68 let results = project_index
69 .read_with(&cx, |project_index, cx| {
70 project_index.search(argument.clone(), 5, cx)
71 })?
72 .await?;
73
74 let mut loaded_results = Vec::new();
75 for result in results {
76 let (full_path, file_content) =
77 result.worktree.read_with(&cx, |worktree, _cx| {
78 let entry_abs_path = worktree.abs_path().join(&result.path);
79 let mut entry_full_path = PathBuf::from(worktree.root_name());
80 entry_full_path.push(&result.path);
81 let file_content = async {
82 let entry_abs_path = entry_abs_path;
83 fs.load(&entry_abs_path).await
84 };
85 (entry_full_path, file_content)
86 })?;
87 if let Some(file_content) = file_content.await.log_err() {
88 loaded_results.push((result, full_path, file_content));
89 }
90 }
91
92 let output = cx
93 .background_executor()
94 .spawn(async move {
95 let mut text = format!("Search results for {argument}:\n");
96 let mut sections = Vec::new();
97 for (result, full_path, file_content) in loaded_results {
98 let range_start = result.range.start.min(file_content.len());
99 let range_end = result.range.end.min(file_content.len());
100
101 let start_line =
102 file_content[0..range_start].matches('\n').count() as u32 + 1;
103 let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
104 let start_line_byte_offset = file_content[0..range_start]
105 .rfind('\n')
106 .map(|pos| pos + 1)
107 .unwrap_or_default();
108 let end_line_byte_offset = file_content[range_end..]
109 .find('\n')
110 .map(|pos| range_end + pos)
111 .unwrap_or_else(|| file_content.len());
112
113 let section_start_ix = text.len();
114 writeln!(
115 text,
116 "```{}:{}-{}",
117 result.path.display(),
118 start_line,
119 end_line,
120 )
121 .unwrap();
122 let mut excerpt =
123 file_content[start_line_byte_offset..end_line_byte_offset].to_string();
124 LineEnding::normalize(&mut excerpt);
125 text.push_str(&excerpt);
126 writeln!(text, "\n```\n").unwrap();
127 let section_end_ix = text.len() - 1;
128
129 sections.push(SlashCommandOutputSection {
130 range: section_start_ix..section_end_ix,
131 render_placeholder: Arc::new(move |id, unfold, _| {
132 FilePlaceholder {
133 id,
134 path: Some(full_path.clone()),
135 line_range: Some(start_line..end_line),
136 unfold,
137 }
138 .into_any_element()
139 }),
140 });
141 }
142
143 let argument = SharedString::from(argument);
144 sections.push(SlashCommandOutputSection {
145 range: 0..text.len(),
146 render_placeholder: Arc::new(move |id, unfold, _cx| {
147 ButtonLike::new(id)
148 .style(ButtonStyle::Filled)
149 .layer(ElevationIndex::ElevatedSurface)
150 .child(Icon::new(IconName::MagnifyingGlass))
151 .child(Label::new(argument.clone()))
152 .on_click(move |_, cx| unfold(cx))
153 .into_any_element()
154 }),
155 });
156
157 SlashCommandOutput { text, sections }
158 })
159 .await;
160
161 Ok(output)
162 })
163 }
164}