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}