search_command.rs

  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        "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(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}