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