1use super::{SlashCommand, SlashCommandOutput};
2use anyhow::{anyhow, Result};
3use assistant_slash_command::SlashCommandOutputSection;
4use fuzzy::PathMatch;
5use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
6use language::{LineEnding, LspAdapterDelegate};
7use project::PathMatchCandidateSet;
8use std::{
9 ops::Range,
10 path::{Path, PathBuf},
11 sync::{atomic::AtomicBool, Arc},
12};
13use ui::{prelude::*, ButtonLike, ElevationIndex};
14use workspace::Workspace;
15
16pub(crate) struct FileSlashCommand;
17
18impl FileSlashCommand {
19 fn search_paths(
20 &self,
21 query: String,
22 cancellation_flag: Arc<AtomicBool>,
23 workspace: &View<Workspace>,
24 cx: &mut AppContext,
25 ) -> Task<Vec<PathMatch>> {
26 if query.is_empty() {
27 let workspace = workspace.read(cx);
28 let project = workspace.project().read(cx);
29 let entries = workspace.recent_navigation_history(Some(10), cx);
30 let path_prefix: Arc<str> = "".into();
31 Task::ready(
32 entries
33 .into_iter()
34 .filter_map(|(entry, _)| {
35 let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
36 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
37 full_path.push(&entry.path);
38 Some(PathMatch {
39 score: 0.,
40 positions: Vec::new(),
41 worktree_id: entry.worktree_id.to_usize(),
42 path: full_path.into(),
43 path_prefix: path_prefix.clone(),
44 distance_to_relative_ancestor: 0,
45 })
46 })
47 .collect(),
48 )
49 } else {
50 let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
51 let candidate_sets = worktrees
52 .into_iter()
53 .map(|worktree| {
54 let worktree = worktree.read(cx);
55 PathMatchCandidateSet {
56 snapshot: worktree.snapshot(),
57 include_ignored: worktree
58 .root_entry()
59 .map_or(false, |entry| entry.is_ignored),
60 include_root_name: true,
61 candidates: project::Candidates::Files,
62 }
63 })
64 .collect::<Vec<_>>();
65
66 let executor = cx.background_executor().clone();
67 cx.foreground_executor().spawn(async move {
68 fuzzy::match_path_sets(
69 candidate_sets.as_slice(),
70 query.as_str(),
71 None,
72 false,
73 100,
74 &cancellation_flag,
75 executor,
76 )
77 .await
78 })
79 }
80 }
81}
82
83impl SlashCommand for FileSlashCommand {
84 fn name(&self) -> String {
85 "file".into()
86 }
87
88 fn description(&self) -> String {
89 "insert file".into()
90 }
91
92 fn menu_text(&self) -> String {
93 "Insert File".into()
94 }
95
96 fn requires_argument(&self) -> bool {
97 true
98 }
99
100 fn complete_argument(
101 &self,
102 query: String,
103 cancellation_flag: Arc<AtomicBool>,
104 workspace: Option<WeakView<Workspace>>,
105 cx: &mut AppContext,
106 ) -> Task<Result<Vec<String>>> {
107 let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
108 return Task::ready(Err(anyhow!("workspace was dropped")));
109 };
110
111 let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
112 cx.background_executor().spawn(async move {
113 Ok(paths
114 .await
115 .into_iter()
116 .map(|path_match| {
117 format!(
118 "{}{}",
119 path_match.path_prefix,
120 path_match.path.to_string_lossy()
121 )
122 })
123 .collect())
124 })
125 }
126
127 fn run(
128 self: Arc<Self>,
129 argument: Option<&str>,
130 workspace: WeakView<Workspace>,
131 _delegate: Arc<dyn LspAdapterDelegate>,
132 cx: &mut WindowContext,
133 ) -> Task<Result<SlashCommandOutput>> {
134 let Some(workspace) = workspace.upgrade() else {
135 return Task::ready(Err(anyhow!("workspace was dropped")));
136 };
137
138 let Some(argument) = argument else {
139 return Task::ready(Err(anyhow!("missing path")));
140 };
141
142 let path = PathBuf::from(argument);
143 let abs_path = workspace
144 .read(cx)
145 .visible_worktrees(cx)
146 .find_map(|worktree| {
147 let worktree = worktree.read(cx);
148 let worktree_root_path = Path::new(worktree.root_name());
149 let relative_path = path.strip_prefix(worktree_root_path).ok()?;
150 worktree.absolutize(&relative_path).ok()
151 });
152
153 let Some(abs_path) = abs_path else {
154 return Task::ready(Err(anyhow!("missing path")));
155 };
156
157 let fs = workspace.read(cx).app_state().fs.clone();
158 let argument = argument.to_string();
159 let text = cx.background_executor().spawn(async move {
160 let mut content = fs.load(&abs_path).await?;
161 LineEnding::normalize(&mut content);
162 let mut output = String::with_capacity(argument.len() + content.len() + 9);
163 output.push_str("```");
164 output.push_str(&argument);
165 output.push('\n');
166 output.push_str(&content);
167 if !output.ends_with('\n') {
168 output.push('\n');
169 }
170 output.push_str("```");
171 anyhow::Ok(output)
172 });
173 cx.foreground_executor().spawn(async move {
174 let text = text.await?;
175 let range = 0..text.len();
176 Ok(SlashCommandOutput {
177 text,
178 sections: vec![SlashCommandOutputSection {
179 range,
180 render_placeholder: Arc::new(move |id, unfold, _cx| {
181 FilePlaceholder {
182 path: Some(path.clone()),
183 line_range: None,
184 id,
185 unfold,
186 }
187 .into_any_element()
188 }),
189 }],
190 run_commands_in_text: false,
191 })
192 })
193 }
194}
195
196#[derive(IntoElement)]
197pub struct FilePlaceholder {
198 pub path: Option<PathBuf>,
199 pub line_range: Option<Range<u32>>,
200 pub id: ElementId,
201 pub unfold: Arc<dyn Fn(&mut WindowContext)>,
202}
203
204impl RenderOnce for FilePlaceholder {
205 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
206 let unfold = self.unfold;
207 let title = if let Some(path) = self.path.as_ref() {
208 SharedString::from(path.to_string_lossy().to_string())
209 } else {
210 SharedString::from("untitled")
211 };
212
213 ButtonLike::new(self.id)
214 .style(ButtonStyle::Filled)
215 .layer(ElevationIndex::ElevatedSurface)
216 .child(Icon::new(IconName::File))
217 .child(Label::new(title))
218 .when_some(self.line_range, |button, line_range| {
219 button.child(Label::new(":")).child(Label::new(format!(
220 "{}-{}",
221 line_range.start, line_range.end
222 )))
223 })
224 .on_click(move |_, cx| unfold(cx))
225 }
226}