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