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, LineEnding, 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 "semantic search".into()
38 }
39
40 fn menu_text(&self) -> String {
41 "Semantic Search".into()
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(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 LoadedSearchResult {
116 path,
117 range,
118 full_path,
119 file_content,
120 row_range,
121 } in loaded_results
122 {
123 let section_start_ix = text.len();
124 text.push_str(&codeblock_fence_for_path(
125 Some(&path),
126 Some(row_range.clone()),
127 ));
128
129 let mut excerpt = file_content[range].to_string();
130 LineEnding::normalize(&mut excerpt);
131 text.push_str(&excerpt);
132 writeln!(text, "\n```\n").unwrap();
133 let section_end_ix = text.len() - 1;
134 sections.push(build_entry_output_section(
135 section_start_ix..section_end_ix,
136 Some(&full_path),
137 false,
138 Some(row_range.start() + 1..row_range.end() + 1),
139 ));
140 }
141
142 let query = SharedString::from(query);
143 sections.push(SlashCommandOutputSection {
144 range: 0..text.len(),
145 icon: IconName::MagnifyingGlass,
146 label: query,
147 metadata: None,
148 });
149
150 SlashCommandOutput {
151 text,
152 sections,
153 run_commands_in_text: false,
154 }
155 })
156 .await;
157
158 Ok(output)
159 })
160 }
161}