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
28pub struct PathSearchTool;
29
30impl Tool for PathSearchTool {
31    fn name(&self) -> String {
32        "path-search".into()
33    }
34
35    fn description(&self) -> String {
36        include_str!("./path_search_tool/description.md").into()
37    }
38
39    fn input_schema(&self) -> serde_json::Value {
40        let schema = schemars::schema_for!(PathSearchToolInput);
41        serde_json::to_value(&schema).unwrap()
42    }
43
44    fn run(
45        self: Arc<Self>,
46        input: serde_json::Value,
47        _messages: &[LanguageModelRequestMessage],
48        project: Entity<Project>,
49        _action_log: Entity<ActionLog>,
50        cx: &mut App,
51    ) -> Task<Result<String>> {
52        let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
53            Ok(input) => input.glob,
54            Err(err) => return Task::ready(Err(anyhow!(err))),
55        };
56        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
57            Ok(matcher) => matcher,
58            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
59        };
60        let snapshots: Vec<Snapshot> = project
61            .read(cx)
62            .worktrees(cx)
63            .map(|worktree| worktree.read(cx).snapshot())
64            .collect();
65
66        cx.background_spawn(async move {
67            let mut matches = Vec::new();
68
69            for worktree in snapshots {
70                let root_name = worktree.root_name();
71
72                // Don't consider ignored entries.
73                for entry in worktree.entries(false, 0) {
74                    if path_matcher.is_match(&entry.path) {
75                        matches.push(
76                            PathBuf::from(root_name)
77                                .join(&entry.path)
78                                .to_string_lossy()
79                                .to_string(),
80                        );
81                    }
82                }
83            }
84
85            if matches.is_empty() {
86                Ok(format!("No paths in the project matched the glob {glob:?}"))
87            } else {
88                // Sort to group entries in the same directory together.
89                matches.sort();
90                Ok(matches.join("\n"))
91            }
92        })
93    }
94}