1use super::{SlashCommand, SlashCommandOutput};
2use anyhow::Result;
3use fuzzy::PathMatch;
4use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
5use language::LspAdapterDelegate;
6use project::{PathMatchCandidateSet, Project};
7use std::{
8 path::{Path, PathBuf},
9 sync::{atomic::AtomicBool, Arc},
10};
11use ui::{prelude::*, ButtonLike, ElevationIndex};
12use workspace::Workspace;
13
14pub(crate) struct FileSlashCommand {
15 project: Model<Project>,
16}
17
18impl FileSlashCommand {
19 pub fn new(project: Model<Project>) -> Self {
20 Self { project }
21 }
22
23 fn search_paths(
24 &self,
25 query: String,
26 cancellation_flag: Arc<AtomicBool>,
27 cx: &mut AppContext,
28 ) -> Task<Vec<PathMatch>> {
29 let worktrees = self
30 .project
31 .read(cx)
32 .visible_worktrees(cx)
33 .collect::<Vec<_>>();
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: true,
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 a file".into()
72 }
73
74 fn tooltip_text(&self) -> String {
75 "insert file".into()
76 }
77
78 fn requires_argument(&self) -> bool {
79 true
80 }
81
82 fn complete_argument(
83 &self,
84 query: String,
85 cancellation_flag: Arc<AtomicBool>,
86 cx: &mut AppContext,
87 ) -> gpui::Task<Result<Vec<String>>> {
88 let paths = self.search_paths(query, cancellation_flag, cx);
89 cx.background_executor().spawn(async move {
90 Ok(paths
91 .await
92 .into_iter()
93 .map(|path_match| {
94 format!(
95 "{}{}",
96 path_match.path_prefix,
97 path_match.path.to_string_lossy()
98 )
99 })
100 .collect())
101 })
102 }
103
104 fn run(
105 self: Arc<Self>,
106 argument: Option<&str>,
107 _workspace: WeakView<Workspace>,
108 _delegate: Arc<dyn LspAdapterDelegate>,
109 cx: &mut WindowContext,
110 ) -> Task<Result<SlashCommandOutput>> {
111 let project = self.project.read(cx);
112 let Some(argument) = argument else {
113 return Task::ready(Err(anyhow::anyhow!("missing path")));
114 };
115
116 let path = PathBuf::from(argument);
117 let abs_path = project.worktrees().find_map(|worktree| {
118 let worktree = worktree.read(cx);
119 let worktree_root_path = Path::new(worktree.root_name());
120 let relative_path = path.strip_prefix(worktree_root_path).ok()?;
121 worktree.absolutize(&relative_path).ok()
122 });
123
124 let Some(abs_path) = abs_path else {
125 return Task::ready(Err(anyhow::anyhow!("missing path")));
126 };
127
128 let fs = project.fs().clone();
129 let argument = argument.to_string();
130 let text = cx.background_executor().spawn(async move {
131 let content = fs.load(&abs_path).await?;
132 let mut output = String::with_capacity(argument.len() + content.len() + 9);
133 output.push_str("```");
134 output.push_str(&argument);
135 output.push('\n');
136 output.push_str(&content);
137 if !output.ends_with('\n') {
138 output.push('\n');
139 }
140 output.push_str("```");
141 anyhow::Ok(output)
142 });
143 cx.foreground_executor().spawn(async move {
144 let text = text.await?;
145 Ok(SlashCommandOutput {
146 text,
147 render_placeholder: Arc::new(move |id, unfold, _cx| {
148 FilePlaceholder {
149 path: Some(path.clone()),
150 id,
151 unfold,
152 }
153 .into_any_element()
154 }),
155 })
156 })
157 }
158}
159
160#[derive(IntoElement)]
161pub struct FilePlaceholder {
162 pub path: Option<PathBuf>,
163 pub id: ElementId,
164 pub unfold: Arc<dyn Fn(&mut WindowContext)>,
165}
166
167impl RenderOnce for FilePlaceholder {
168 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
169 let unfold = self.unfold;
170 let title = if let Some(path) = self.path.as_ref() {
171 SharedString::from(path.to_string_lossy().to_string())
172 } else {
173 SharedString::from("untitled")
174 };
175
176 ButtonLike::new(self.id)
177 .style(ButtonStyle::Filled)
178 .layer(ElevationIndex::ElevatedSurface)
179 .child(Icon::new(IconName::File))
180 .child(Label::new(title))
181 .on_click(move |_, cx| unfold(cx))
182 }
183}