1use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
2use anyhow::Result;
3use futures::channel::oneshot;
4use fuzzy::PathMatch;
5use gpui::{AppContext, Model, Task};
6use project::{PathMatchCandidateSet, Project};
7use std::{
8 path::Path,
9 sync::{atomic::AtomicBool, Arc},
10};
11
12pub(crate) struct FileSlashCommand {
13 project: Model<Project>,
14}
15
16impl FileSlashCommand {
17 pub fn new(project: Model<Project>) -> Self {
18 Self { project }
19 }
20
21 fn search_paths(
22 &self,
23 query: String,
24 cancellation_flag: Arc<AtomicBool>,
25 cx: &mut AppContext,
26 ) -> Task<Vec<PathMatch>> {
27 let worktrees = self
28 .project
29 .read(cx)
30 .visible_worktrees(cx)
31 .collect::<Vec<_>>();
32 let include_root_name = worktrees.len() > 1;
33 let candidate_sets = worktrees
34 .into_iter()
35 .map(|worktree| {
36 let worktree = worktree.read(cx);
37 PathMatchCandidateSet {
38 snapshot: worktree.snapshot(),
39 include_ignored: worktree
40 .root_entry()
41 .map_or(false, |entry| entry.is_ignored),
42 include_root_name,
43 directories_only: false,
44 }
45 })
46 .collect::<Vec<_>>();
47
48 let executor = cx.background_executor().clone();
49 cx.foreground_executor().spawn(async move {
50 fuzzy::match_path_sets(
51 candidate_sets.as_slice(),
52 query.as_str(),
53 None,
54 false,
55 100,
56 &cancellation_flag,
57 executor,
58 )
59 .await
60 })
61 }
62}
63
64impl SlashCommand for FileSlashCommand {
65 fn name(&self) -> String {
66 "file".into()
67 }
68
69 fn description(&self) -> String {
70 "insert an entire file".into()
71 }
72
73 fn requires_argument(&self) -> bool {
74 true
75 }
76
77 fn complete_argument(
78 &self,
79 query: String,
80 cancellation_flag: Arc<AtomicBool>,
81 cx: &mut AppContext,
82 ) -> gpui::Task<Result<Vec<String>>> {
83 let paths = self.search_paths(query, cancellation_flag, cx);
84 cx.background_executor().spawn(async move {
85 Ok(paths
86 .await
87 .into_iter()
88 .map(|path_match| {
89 format!(
90 "{}{}",
91 path_match.path_prefix,
92 path_match.path.to_string_lossy()
93 )
94 })
95 .collect())
96 })
97 }
98
99 fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
100 let project = self.project.read(cx);
101 let Some(argument) = argument else {
102 return SlashCommandInvocation {
103 output: Task::ready(Err(anyhow::anyhow!("missing path"))),
104 invalidated: oneshot::channel().1,
105 cleanup: SlashCommandCleanup::default(),
106 };
107 };
108
109 let path = Path::new(argument);
110 let abs_path = project.worktrees().find_map(|worktree| {
111 let worktree = worktree.read(cx);
112 worktree.entry_for_path(path)?;
113 worktree.absolutize(path).ok()
114 });
115
116 let Some(abs_path) = abs_path else {
117 return SlashCommandInvocation {
118 output: Task::ready(Err(anyhow::anyhow!("missing path"))),
119 invalidated: oneshot::channel().1,
120 cleanup: SlashCommandCleanup::default(),
121 };
122 };
123
124 let fs = project.fs().clone();
125 let argument = argument.to_string();
126 let output = cx.background_executor().spawn(async move {
127 let content = fs.load(&abs_path).await?;
128 let mut output = String::with_capacity(argument.len() + content.len() + 9);
129 output.push_str("```");
130 output.push_str(&argument);
131 output.push('\n');
132 output.push_str(&content);
133 if !output.ends_with('\n') {
134 output.push('\n');
135 }
136 output.push_str("```");
137 Ok(output)
138 });
139 SlashCommandInvocation {
140 output,
141 invalidated: oneshot::channel().1,
142 cleanup: SlashCommandCleanup::default(),
143 }
144 }
145}