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