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