1use super::{
2 diagnostics_command::write_single_file_diagnostics,
3 file_command::{build_entry_output_section, codeblock_fence_for_path},
4 SlashCommand, SlashCommandOutput,
5};
6use anyhow::{Context, Result};
7use assistant_slash_command::ArgumentCompletion;
8use collections::{HashMap, HashSet};
9use editor::Editor;
10use futures::future::join_all;
11use gpui::{Entity, Task, WeakView};
12use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
13use std::{
14 fmt::Write,
15 path::PathBuf,
16 sync::{atomic::AtomicBool, Arc},
17};
18use ui::{ActiveTheme, WindowContext};
19use workspace::Workspace;
20
21pub(crate) struct TabSlashCommand;
22
23const ALL_TABS_COMPLETION_ITEM: &str = "all";
24
25impl SlashCommand for TabSlashCommand {
26 fn name(&self) -> String {
27 "tab".into()
28 }
29
30 fn description(&self) -> String {
31 "insert open tabs (active tab by default)".to_owned()
32 }
33
34 fn menu_text(&self) -> String {
35 "Insert Open Tabs".to_owned()
36 }
37
38 fn requires_argument(&self) -> bool {
39 false
40 }
41
42 fn accepts_arguments(&self) -> bool {
43 true
44 }
45
46 fn complete_argument(
47 self: Arc<Self>,
48 arguments: &[String],
49 cancel: Arc<AtomicBool>,
50 workspace: Option<WeakView<Workspace>>,
51 cx: &mut WindowContext,
52 ) -> Task<Result<Vec<ArgumentCompletion>>> {
53 let mut has_all_tabs_completion_item = false;
54 let argument_set = arguments
55 .iter()
56 .filter(|argument| {
57 if has_all_tabs_completion_item || ALL_TABS_COMPLETION_ITEM == argument.as_str() {
58 has_all_tabs_completion_item = true;
59 false
60 } else {
61 true
62 }
63 })
64 .cloned()
65 .collect::<HashSet<_>>();
66 if has_all_tabs_completion_item {
67 return Task::ready(Ok(Vec::new()));
68 }
69
70 let active_item_path = workspace.as_ref().and_then(|workspace| {
71 workspace
72 .update(cx, |workspace, cx| {
73 let snapshot = active_item_buffer(workspace, cx).ok()?;
74 snapshot.resolve_file_path(cx, true)
75 })
76 .ok()
77 .flatten()
78 });
79 let current_query = arguments.last().cloned().unwrap_or_default();
80 let tab_items_search =
81 tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
82
83 let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
84 cx.spawn(|_| async move {
85 let tab_items = tab_items_search.await?;
86 let run_command = tab_items.len() == 1;
87 let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
88 let path_string = path.as_deref()?.to_string_lossy().to_string();
89 if argument_set.contains(&path_string) {
90 return None;
91 }
92 if active_item_path.is_some() && active_item_path == path {
93 return None;
94 }
95 let label = create_tab_completion_label(path.as_ref()?, comment_id);
96 Some(ArgumentCompletion {
97 label,
98 new_text: path_string,
99 replace_previous_arguments: false,
100 after_completion: run_command.into(),
101 })
102 });
103
104 let active_item_completion = active_item_path
105 .as_deref()
106 .map(|active_item_path| {
107 let path_string = active_item_path.to_string_lossy().to_string();
108 let label = create_tab_completion_label(active_item_path, comment_id);
109 ArgumentCompletion {
110 label,
111 new_text: path_string,
112 replace_previous_arguments: false,
113 after_completion: run_command.into(),
114 }
115 })
116 .filter(|completion| !argument_set.contains(&completion.new_text));
117
118 Ok(active_item_completion
119 .into_iter()
120 .chain(Some(ArgumentCompletion {
121 label: ALL_TABS_COMPLETION_ITEM.into(),
122 new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
123 replace_previous_arguments: false,
124 after_completion: true.into(),
125 }))
126 .chain(tab_completion_items)
127 .collect())
128 })
129 }
130
131 fn run(
132 self: Arc<Self>,
133 arguments: &[String],
134 workspace: WeakView<Workspace>,
135 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
136 cx: &mut WindowContext,
137 ) -> Task<Result<SlashCommandOutput>> {
138 let tab_items_search = tab_items_for_queries(
139 Some(workspace),
140 arguments,
141 Arc::new(AtomicBool::new(false)),
142 true,
143 cx,
144 );
145
146 cx.background_executor().spawn(async move {
147 let mut sections = Vec::new();
148 let mut text = String::new();
149 let mut has_diagnostics = false;
150 for (full_path, buffer, _) in tab_items_search.await? {
151 let section_start_ix = text.len();
152 text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
153 for chunk in buffer.as_rope().chunks() {
154 text.push_str(chunk);
155 }
156 if !text.ends_with('\n') {
157 text.push('\n');
158 }
159 writeln!(text, "```").unwrap();
160 if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
161 has_diagnostics = true;
162 }
163 if !text.ends_with('\n') {
164 text.push('\n');
165 }
166
167 let section_end_ix = text.len() - 1;
168 sections.push(build_entry_output_section(
169 section_start_ix..section_end_ix,
170 full_path.as_deref(),
171 false,
172 None,
173 ));
174 }
175
176 Ok(SlashCommandOutput {
177 text,
178 sections,
179 run_commands_in_text: has_diagnostics,
180 })
181 })
182 }
183}
184
185fn tab_items_for_queries(
186 workspace: Option<WeakView<Workspace>>,
187 queries: &[String],
188 cancel: Arc<AtomicBool>,
189 strict_match: bool,
190 cx: &mut WindowContext,
191) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
192 let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
193 let queries = queries.to_owned();
194 cx.spawn(|mut cx| async move {
195 let mut open_buffers =
196 workspace
197 .context("no workspace")?
198 .update(&mut cx, |workspace, cx| {
199 if strict_match && empty_query {
200 let snapshot = active_item_buffer(workspace, cx)?;
201 let full_path = snapshot.resolve_file_path(cx, true);
202 return anyhow::Ok(vec![(full_path, snapshot, 0)]);
203 }
204
205 let mut timestamps_by_entity_id = HashMap::default();
206 let mut visited_buffers = HashSet::default();
207 let mut open_buffers = Vec::new();
208
209 for pane in workspace.panes() {
210 let pane = pane.read(cx);
211 for entry in pane.activation_history() {
212 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
213 }
214 }
215
216 for editor in workspace.items_of_type::<Editor>(cx) {
217 if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
218 if let Some(timestamp) =
219 timestamps_by_entity_id.get(&editor.entity_id())
220 {
221 if visited_buffers.insert(buffer.read(cx).remote_id()) {
222 let snapshot = buffer.read(cx).snapshot();
223 let full_path = snapshot.resolve_file_path(cx, true);
224 open_buffers.push((full_path, snapshot, *timestamp));
225 }
226 }
227 }
228 }
229
230 Ok(open_buffers)
231 })??;
232
233 let background_executor = cx.background_executor().clone();
234 cx.background_executor()
235 .spawn(async move {
236 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
237 if empty_query
238 || queries
239 .iter()
240 .any(|query| query == ALL_TABS_COMPLETION_ITEM)
241 {
242 return Ok(open_buffers);
243 }
244
245 let matched_items = if strict_match {
246 let match_candidates = open_buffers
247 .iter()
248 .enumerate()
249 .filter_map(|(id, (full_path, ..))| {
250 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
251 Some((id, path_string))
252 })
253 .fold(HashMap::default(), |mut candidates, (id, path_string)| {
254 candidates
255 .entry(path_string)
256 .or_insert_with(Vec::new)
257 .push(id);
258 candidates
259 });
260
261 queries
262 .iter()
263 .filter_map(|query| match_candidates.get(query))
264 .flatten()
265 .copied()
266 .filter_map(|id| open_buffers.get(id))
267 .cloned()
268 .collect()
269 } else {
270 let match_candidates = open_buffers
271 .iter()
272 .enumerate()
273 .filter_map(|(id, (full_path, ..))| {
274 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
275 Some(fuzzy::StringMatchCandidate {
276 id,
277 char_bag: path_string.as_str().into(),
278 string: path_string,
279 })
280 })
281 .collect::<Vec<_>>();
282 let mut processed_matches = HashSet::default();
283 let file_queries = queries.iter().map(|query| {
284 fuzzy::match_strings(
285 &match_candidates,
286 query,
287 true,
288 usize::MAX,
289 &cancel,
290 background_executor.clone(),
291 )
292 });
293
294 join_all(file_queries)
295 .await
296 .into_iter()
297 .flatten()
298 .filter(|string_match| processed_matches.insert(string_match.candidate_id))
299 .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
300 .cloned()
301 .collect()
302 };
303 Ok(matched_items)
304 })
305 .await
306 })
307}
308
309fn active_item_buffer(
310 workspace: &mut Workspace,
311 cx: &mut ui::ViewContext<Workspace>,
312) -> anyhow::Result<BufferSnapshot> {
313 let active_editor = workspace
314 .active_item(cx)
315 .context("no active item")?
316 .downcast::<Editor>()
317 .context("active item is not an editor")?;
318 let snapshot = active_editor
319 .read(cx)
320 .buffer()
321 .read(cx)
322 .as_singleton()
323 .context("active editor is not a singleton buffer")?
324 .read(cx)
325 .snapshot();
326 Ok(snapshot)
327}
328
329fn create_tab_completion_label(
330 path: &std::path::Path,
331 comment_id: Option<HighlightId>,
332) -> CodeLabel {
333 let file_name = path
334 .file_name()
335 .map(|f| f.to_string_lossy())
336 .unwrap_or_default();
337 let parent_path = path
338 .parent()
339 .map(|p| p.to_string_lossy())
340 .unwrap_or_default();
341 let mut label = CodeLabel::default();
342 label.push_str(&file_name, None);
343 label.push_str(" ", None);
344 label.push_str(&parent_path, comment_id);
345 label.filter_range = 0..file_name.len();
346 label
347}