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, LspAdapterDelegate};
13use std::{
14 fmt::Write,
15 path::PathBuf,
16 sync::{atomic::AtomicBool, Arc},
17};
18use ui::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 cx.spawn(|_| async move {
83 let tab_items = tab_items_search.await?;
84 let run_command = tab_items.len() == 1;
85 let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
86 let path_string = path.as_deref()?.to_string_lossy().to_string();
87 if argument_set.contains(&path_string) {
88 return None;
89 }
90 if active_item_path.is_some() && active_item_path == path {
91 return None;
92 }
93 Some(ArgumentCompletion {
94 label: path_string.clone().into(),
95 new_text: path_string,
96 replace_previous_arguments: false,
97 after_completion: run_command.into(),
98 })
99 });
100
101 let active_item_completion = active_item_path
102 .as_deref()
103 .map(|active_item_path| active_item_path.to_string_lossy().to_string())
104 .filter(|path_string| !argument_set.contains(path_string))
105 .map(|path_string| ArgumentCompletion {
106 label: path_string.clone().into(),
107 new_text: path_string,
108 replace_previous_arguments: false,
109 after_completion: run_command.into(),
110 });
111
112 Ok(active_item_completion
113 .into_iter()
114 .chain(Some(ArgumentCompletion {
115 label: ALL_TABS_COMPLETION_ITEM.into(),
116 new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
117 replace_previous_arguments: false,
118 after_completion: true.into(),
119 }))
120 .chain(tab_completion_items)
121 .collect())
122 })
123 }
124
125 fn run(
126 self: Arc<Self>,
127 arguments: &[String],
128 workspace: WeakView<Workspace>,
129 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
130 cx: &mut WindowContext,
131 ) -> Task<Result<SlashCommandOutput>> {
132 let tab_items_search = tab_items_for_queries(
133 Some(workspace),
134 arguments,
135 Arc::new(AtomicBool::new(false)),
136 true,
137 cx,
138 );
139
140 cx.background_executor().spawn(async move {
141 let mut sections = Vec::new();
142 let mut text = String::new();
143 let mut has_diagnostics = false;
144 for (full_path, buffer, _) in tab_items_search.await? {
145 let section_start_ix = text.len();
146 text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
147 for chunk in buffer.as_rope().chunks() {
148 text.push_str(chunk);
149 }
150 if !text.ends_with('\n') {
151 text.push('\n');
152 }
153 writeln!(text, "```").unwrap();
154 if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
155 has_diagnostics = true;
156 }
157 if !text.ends_with('\n') {
158 text.push('\n');
159 }
160
161 let section_end_ix = text.len() - 1;
162 sections.push(build_entry_output_section(
163 section_start_ix..section_end_ix,
164 full_path.as_deref(),
165 false,
166 None,
167 ));
168 }
169
170 Ok(SlashCommandOutput {
171 text,
172 sections,
173 run_commands_in_text: has_diagnostics,
174 })
175 })
176 }
177}
178
179fn tab_items_for_queries(
180 workspace: Option<WeakView<Workspace>>,
181 queries: &[String],
182 cancel: Arc<AtomicBool>,
183 strict_match: bool,
184 cx: &mut WindowContext,
185) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
186 let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
187 let queries = queries.to_owned();
188 cx.spawn(|mut cx| async move {
189 let mut open_buffers =
190 workspace
191 .context("no workspace")?
192 .update(&mut cx, |workspace, cx| {
193 if strict_match && empty_query {
194 let snapshot = active_item_buffer(workspace, cx)?;
195 let full_path = snapshot.resolve_file_path(cx, true);
196 return anyhow::Ok(vec![(full_path, snapshot, 0)]);
197 }
198
199 let mut timestamps_by_entity_id = HashMap::default();
200 let mut visited_buffers = HashSet::default();
201 let mut open_buffers = Vec::new();
202
203 for pane in workspace.panes() {
204 let pane = pane.read(cx);
205 for entry in pane.activation_history() {
206 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
207 }
208 }
209
210 for editor in workspace.items_of_type::<Editor>(cx) {
211 if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
212 if let Some(timestamp) =
213 timestamps_by_entity_id.get(&editor.entity_id())
214 {
215 if visited_buffers.insert(buffer.read(cx).remote_id()) {
216 let snapshot = buffer.read(cx).snapshot();
217 let full_path = snapshot.resolve_file_path(cx, true);
218 open_buffers.push((full_path, snapshot, *timestamp));
219 }
220 }
221 }
222 }
223
224 Ok(open_buffers)
225 })??;
226
227 let background_executor = cx.background_executor().clone();
228 cx.background_executor()
229 .spawn(async move {
230 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
231 if empty_query
232 || queries
233 .iter()
234 .any(|query| query == ALL_TABS_COMPLETION_ITEM)
235 {
236 return Ok(open_buffers);
237 }
238
239 let matched_items = if strict_match {
240 let match_candidates = open_buffers
241 .iter()
242 .enumerate()
243 .filter_map(|(id, (full_path, ..))| {
244 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
245 Some((id, path_string))
246 })
247 .fold(HashMap::default(), |mut candidates, (id, path_string)| {
248 candidates
249 .entry(path_string)
250 .or_insert_with(|| Vec::new())
251 .push(id);
252 candidates
253 });
254
255 queries
256 .iter()
257 .filter_map(|query| match_candidates.get(query))
258 .flatten()
259 .copied()
260 .filter_map(|id| open_buffers.get(id))
261 .cloned()
262 .collect()
263 } else {
264 let match_candidates = open_buffers
265 .iter()
266 .enumerate()
267 .filter_map(|(id, (full_path, ..))| {
268 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
269 Some(fuzzy::StringMatchCandidate {
270 id,
271 char_bag: path_string.as_str().into(),
272 string: path_string,
273 })
274 })
275 .collect::<Vec<_>>();
276 let mut processed_matches = HashSet::default();
277 let file_queries = queries.iter().map(|query| {
278 fuzzy::match_strings(
279 &match_candidates,
280 query,
281 true,
282 usize::MAX,
283 &cancel,
284 background_executor.clone(),
285 )
286 });
287
288 join_all(file_queries)
289 .await
290 .into_iter()
291 .flatten()
292 .filter(|string_match| processed_matches.insert(string_match.candidate_id))
293 .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
294 .cloned()
295 .collect()
296 };
297 Ok(matched_items)
298 })
299 .await
300 })
301}
302
303fn active_item_buffer(
304 workspace: &mut Workspace,
305 cx: &mut ui::ViewContext<Workspace>,
306) -> anyhow::Result<BufferSnapshot> {
307 let active_editor = workspace
308 .active_item(cx)
309 .context("no active item")?
310 .downcast::<Editor>()
311 .context("active item is not an editor")?;
312 let snapshot = active_editor
313 .read(cx)
314 .buffer()
315 .read(cx)
316 .as_singleton()
317 .context("active editor is not a singleton buffer")?
318 .read(cx)
319 .snapshot();
320 Ok(snapshot)
321}