search_command.rs

  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}