search_command.rs

  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}