path_search_tool.rs

  1use anyhow::{anyhow, Result};
  2use assistant_tool::{ActionLog, Tool};
  3use gpui::{App, AppContext, Entity, Task};
  4use language_model::LanguageModelRequestMessage;
  5use project::Project;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use std::{path::PathBuf, sync::Arc};
  9use util::paths::PathMatcher;
 10use worktree::Snapshot;
 11
 12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 13pub struct PathSearchToolInput {
 14    /// The glob to search all project paths for.
 15    ///
 16    /// <example>
 17    /// If the project has the following root directories:
 18    ///
 19    /// - directory1/a/something.txt
 20    /// - directory2/a/things.txt
 21    /// - directory3/a/other.txt
 22    ///
 23    /// You can get back the first two paths by providing a glob of "*thing*.txt"
 24    /// </example>
 25    pub glob: String,
 26
 27    /// Optional starting position for paginated results (0-based).
 28    /// When not provided, starts from the beginning.
 29    #[serde(default)]
 30    pub offset: Option<usize>,
 31}
 32
 33const RESULTS_PER_PAGE: usize = 50;
 34
 35pub struct PathSearchTool;
 36
 37impl Tool for PathSearchTool {
 38    fn name(&self) -> String {
 39        "path-search".into()
 40    }
 41
 42    fn needs_confirmation(&self) -> bool {
 43        false
 44    }
 45
 46    fn description(&self) -> String {
 47        include_str!("./path_search_tool/description.md").into()
 48    }
 49
 50    fn input_schema(&self) -> serde_json::Value {
 51        let schema = schemars::schema_for!(PathSearchToolInput);
 52        serde_json::to_value(&schema).unwrap()
 53    }
 54
 55    fn ui_text(&self, input: &serde_json::Value) -> String {
 56        match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
 57            Ok(input) => format!("Find paths matching “`{}`”", input.glob),
 58            Err(_) => "Search paths".to_string(),
 59        }
 60    }
 61
 62    fn run(
 63        self: Arc<Self>,
 64        input: serde_json::Value,
 65        _messages: &[LanguageModelRequestMessage],
 66        project: Entity<Project>,
 67        _action_log: Entity<ActionLog>,
 68        cx: &mut App,
 69    ) -> Task<Result<String>> {
 70        let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
 71            Ok(input) => (input.offset.unwrap_or(0), input.glob),
 72            Err(err) => return Task::ready(Err(anyhow!(err))),
 73        };
 74        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
 75            Ok(matcher) => matcher,
 76            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
 77        };
 78        let snapshots: Vec<Snapshot> = project
 79            .read(cx)
 80            .worktrees(cx)
 81            .map(|worktree| worktree.read(cx).snapshot())
 82            .collect();
 83
 84        cx.background_spawn(async move {
 85            let mut matches = Vec::new();
 86
 87            for worktree in snapshots {
 88                let root_name = worktree.root_name();
 89
 90                // Don't consider ignored entries.
 91                for entry in worktree.entries(false, 0) {
 92                    if path_matcher.is_match(&entry.path) {
 93                        matches.push(
 94                            PathBuf::from(root_name)
 95                                .join(&entry.path)
 96                                .to_string_lossy()
 97                                .to_string(),
 98                        );
 99                    }
100                }
101            }
102
103            if matches.is_empty() {
104                Ok(format!("No paths in the project matched the glob {glob:?}"))
105            } else {
106                // Sort to group entries in the same directory together.
107                matches.sort();
108
109                let total_matches = matches.len();
110                let response = if total_matches > offset + RESULTS_PER_PAGE {
111                  let paginated_matches: Vec<_> = matches
112                      .into_iter()
113                      .skip(offset)
114                      .take(RESULTS_PER_PAGE)
115                      .collect();
116
117                    format!(
118                        "Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n\n{}",
119                        total_matches,
120                        offset + 1,
121                        offset + paginated_matches.len(),
122                        paginated_matches.join("\n")
123                    )
124                } else {
125                    matches.join("\n")
126                };
127
128                Ok(response)
129            }
130        })
131    }
132}