1use anyhow::Result;
2use assistant_slash_command::{
3 ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
4 SlashCommandResult,
5};
6use feature_flags::FeatureFlag;
7use gpui::{AppContext, Task, WeakView};
8use language::{CodeLabel, LspAdapterDelegate};
9use semantic_index::{LoadedSearchResult, SemanticDb};
10use std::{
11 fmt::Write,
12 sync::{atomic::AtomicBool, Arc},
13};
14use ui::{prelude::*, IconName};
15use workspace::Workspace;
16
17use crate::slash_command::create_label_for_command;
18use crate::slash_command::file_command::{build_entry_output_section, codeblock_fence_for_path};
19
20pub(crate) struct SearchSlashCommandFeatureFlag;
21
22impl FeatureFlag for SearchSlashCommandFeatureFlag {
23 const NAME: &'static str = "search-slash-command";
24}
25
26pub(crate) struct SearchSlashCommand;
27
28impl SlashCommand for SearchSlashCommand {
29 fn name(&self) -> String {
30 "search".into()
31 }
32
33 fn label(&self, cx: &AppContext) -> CodeLabel {
34 create_label_for_command("search", &["--n"], cx)
35 }
36
37 fn description(&self) -> String {
38 "Search your project semantically".into()
39 }
40
41 fn icon(&self) -> IconName {
42 IconName::SearchCode
43 }
44
45 fn menu_text(&self) -> String {
46 self.description()
47 }
48
49 fn requires_argument(&self) -> bool {
50 true
51 }
52
53 fn complete_argument(
54 self: Arc<Self>,
55 _arguments: &[String],
56 _cancel: Arc<AtomicBool>,
57 _workspace: Option<WeakView<Workspace>>,
58 _cx: &mut WindowContext,
59 ) -> Task<Result<Vec<ArgumentCompletion>>> {
60 Task::ready(Ok(Vec::new()))
61 }
62
63 fn run(
64 self: Arc<Self>,
65 arguments: &[String],
66 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
67 _context_buffer: language::BufferSnapshot,
68 workspace: WeakView<Workspace>,
69 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
70 cx: &mut WindowContext,
71 ) -> Task<SlashCommandResult> {
72 let Some(workspace) = workspace.upgrade() else {
73 return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
74 };
75 if arguments.is_empty() {
76 return Task::ready(Err(anyhow::anyhow!("missing search query")));
77 };
78
79 let mut limit = None;
80 let mut query = String::new();
81 for part in arguments {
82 if let Some(parameter) = part.strip_prefix("--") {
83 if let Ok(count) = parameter.parse::<usize>() {
84 limit = Some(count);
85 continue;
86 }
87 }
88
89 query.push_str(part);
90 query.push(' ');
91 }
92 query.pop();
93
94 if query.is_empty() {
95 return Task::ready(Err(anyhow::anyhow!("missing search query")));
96 }
97
98 let project = workspace.read(cx).project().clone();
99 let fs = project.read(cx).fs().clone();
100 let Some(project_index) =
101 cx.update_global(|index: &mut SemanticDb, cx| index.project_index(project, cx))
102 else {
103 return Task::ready(Err(anyhow::anyhow!("no project indexer")));
104 };
105
106 cx.spawn(|cx| async move {
107 let results = project_index
108 .read_with(&cx, |project_index, cx| {
109 project_index.search(vec![query.clone()], limit.unwrap_or(5), cx)
110 })?
111 .await?;
112
113 let loaded_results = SemanticDb::load_results(results, &fs, &cx).await?;
114
115 let output = cx
116 .background_executor()
117 .spawn(async move {
118 let mut text = format!("Search results for {query}:\n");
119 let mut sections = Vec::new();
120 for loaded_result in &loaded_results {
121 add_search_result_section(loaded_result, &mut text, &mut sections);
122 }
123
124 let query = SharedString::from(query);
125 sections.push(SlashCommandOutputSection {
126 range: 0..text.len(),
127 icon: IconName::MagnifyingGlass,
128 label: query,
129 metadata: None,
130 });
131
132 SlashCommandOutput {
133 text,
134 sections,
135 run_commands_in_text: false,
136 }
137 .to_event_stream()
138 })
139 .await;
140
141 Ok(output)
142 })
143 }
144}
145
146pub fn add_search_result_section(
147 loaded_result: &LoadedSearchResult,
148 text: &mut String,
149 sections: &mut Vec<SlashCommandOutputSection<usize>>,
150) {
151 let LoadedSearchResult {
152 path,
153 full_path,
154 excerpt_content,
155 row_range,
156 ..
157 } = loaded_result;
158 let section_start_ix = text.len();
159 text.push_str(&codeblock_fence_for_path(
160 Some(&path),
161 Some(row_range.clone()),
162 ));
163
164 text.push_str(&excerpt_content);
165 if !text.ends_with('\n') {
166 text.push('\n');
167 }
168 writeln!(text, "```\n").unwrap();
169 let section_end_ix = text.len() - 1;
170 sections.push(build_entry_output_section(
171 section_start_ix..section_end_ix,
172 Some(&full_path),
173 false,
174 Some(row_range.start() + 1..row_range.end() + 1),
175 ));
176}