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}