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 description(&self) -> String {
 43        include_str!("./path_search_tool/description.md").into()
 44    }
 45
 46    fn input_schema(&self) -> serde_json::Value {
 47        let schema = schemars::schema_for!(PathSearchToolInput);
 48        serde_json::to_value(&schema).unwrap()
 49    }
 50
 51    fn ui_text(&self, input: &serde_json::Value) -> String {
 52        match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
 53            Ok(input) => format!("Find paths matching “`{}`”", input.glob),
 54            Err(_) => "Search paths".to_string(),
 55        }
 56    }
 57
 58    fn run(
 59        self: Arc<Self>,
 60        input: serde_json::Value,
 61        _messages: &[LanguageModelRequestMessage],
 62        project: Entity<Project>,
 63        _action_log: Entity<ActionLog>,
 64        cx: &mut App,
 65    ) -> Task<Result<String>> {
 66        let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
 67            Ok(input) => (input.offset.unwrap_or(0), input.glob),
 68            Err(err) => return Task::ready(Err(anyhow!(err))),
 69        };
 70        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
 71            Ok(matcher) => matcher,
 72            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
 73        };
 74        let snapshots: Vec<Snapshot> = project
 75            .read(cx)
 76            .worktrees(cx)
 77            .map(|worktree| worktree.read(cx).snapshot())
 78            .collect();
 79
 80        cx.background_spawn(async move {
 81            let mut matches = Vec::new();
 82
 83            for worktree in snapshots {
 84                let root_name = worktree.root_name();
 85
 86                // Don't consider ignored entries.
 87                for entry in worktree.entries(false, 0) {
 88                    if path_matcher.is_match(&entry.path) {
 89                        matches.push(
 90                            PathBuf::from(root_name)
 91                                .join(&entry.path)
 92                                .to_string_lossy()
 93                                .to_string(),
 94                        );
 95                    }
 96                }
 97            }
 98
 99            if matches.is_empty() {
100                Ok(format!("No paths in the project matched the glob {glob:?}"))
101            } else {
102                // Sort to group entries in the same directory together.
103                matches.sort();
104
105                let total_matches = matches.len();
106                let response = if total_matches > offset + RESULTS_PER_PAGE {
107                  let paginated_matches: Vec<_> = matches
108                      .into_iter()
109                      .skip(offset)
110                      .take(RESULTS_PER_PAGE)
111                      .collect();
112
113                    format!(
114                        "Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n\n{}",
115                        total_matches,
116                        offset + 1,
117                        offset + paginated_matches.len(),
118                        paginated_matches.join("\n")
119                    )
120                } else {
121                    matches.join("\n")
122                };
123
124                Ok(response)
125            }
126        })
127    }
128}