path_search_tool.rs

 1use anyhow::{anyhow, Result};
 2use assistant_tool::{ActionLog, 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        _action_log: Entity<ActionLog>,
49        cx: &mut App,
50    ) -> Task<Result<String>> {
51        let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
52            Ok(input) => input.glob,
53            Err(err) => return Task::ready(Err(anyhow!(err))),
54        };
55        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
56            Ok(matcher) => matcher,
57            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
58        };
59
60        let mut matches = Vec::new();
61
62        for worktree_handle in project.read(cx).worktrees(cx) {
63            let worktree = worktree_handle.read(cx);
64            let root_name = worktree.root_name();
65
66            // Don't consider ignored entries.
67            for entry in worktree.entries(false, 0) {
68                if path_matcher.is_match(&entry.path) {
69                    matches.push(
70                        PathBuf::from(root_name)
71                            .join(&entry.path)
72                            .to_string_lossy()
73                            .to_string(),
74                    );
75                }
76            }
77        }
78
79        if matches.is_empty() {
80            Task::ready(Ok(format!(
81                "No paths in the project matched the glob {glob:?}"
82            )))
83        } else {
84            // Sort to group entries in the same directory together.
85            matches.sort();
86            Task::ready(Ok(matches.join("\n")))
87        }
88    }
89}