path_search_tool.rs

 1use anyhow::{anyhow, Result};
 2use assistant_tool::Tool;
 3use gpui::{App, 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;
10
11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
12pub struct PathSearchToolInput {
13    /// The glob to search all project paths for.
14    ///
15    /// <example>
16    /// If the project has the following root directories:
17    ///
18    /// - directory1/a/something.txt
19    /// - directory2/a/things.txt
20    /// - directory3/a/other.txt
21    ///
22    /// You can get back the first two paths by providing a glob of "*thing*.txt"
23    /// </example>
24    pub glob: String,
25}
26
27pub struct PathSearchTool;
28
29impl Tool for PathSearchTool {
30    fn name(&self) -> String {
31        "path-search".into()
32    }
33
34    fn description(&self) -> String {
35        include_str!("./path_search_tool/description.md").into()
36    }
37
38    fn input_schema(&self) -> serde_json::Value {
39        let schema = schemars::schema_for!(PathSearchToolInput);
40        serde_json::to_value(&schema).unwrap()
41    }
42
43    fn run(
44        self: Arc<Self>,
45        input: serde_json::Value,
46        _messages: &[LanguageModelRequestMessage],
47        project: Entity<Project>,
48        cx: &mut App,
49    ) -> Task<Result<String>> {
50        let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
51            Ok(input) => input.glob,
52            Err(err) => return Task::ready(Err(anyhow!(err))),
53        };
54        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
55            Ok(matcher) => matcher,
56            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
57        };
58
59        let mut matches = Vec::new();
60
61        for worktree_handle in project.read(cx).worktrees(cx) {
62            let worktree = worktree_handle.read(cx);
63            let root_name = worktree.root_name();
64
65            // Don't consider ignored entries.
66            for entry in worktree.entries(false, 0) {
67                if path_matcher.is_match(&entry.path) {
68                    matches.push(
69                        PathBuf::from(root_name)
70                            .join(&entry.path)
71                            .to_string_lossy()
72                            .to_string(),
73                    );
74                }
75            }
76        }
77
78        if matches.is_empty() {
79            Task::ready(Ok(format!(
80                "No paths in the project matched the glob {glob:?}"
81            )))
82        } else {
83            // Sort to group entries in the same directory together.
84            matches.sort();
85            Task::ready(Ok(matches.join("\n")))
86        }
87    }
88}