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 open_buffers = Vec::new();
201
202 for pane in workspace.panes() {
203 let pane = pane.read(cx);
204 for entry in pane.activation_history() {
205 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
206 }
207 }
208
209 for editor in workspace.items_of_type::<Editor>(cx) {
210 if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
211 if let Some(timestamp) =
212 timestamps_by_entity_id.get(&editor.entity_id())
213 {
214 let snapshot = buffer.read(cx).snapshot();
215 let full_path = snapshot.resolve_file_path(cx, true);
216 open_buffers.push((full_path, snapshot, *timestamp));
217 }
218 }
219 }
220
221 Ok(open_buffers)
222 })??;
223
224 let background_executor = cx.background_executor().clone();
225 cx.background_executor()
226 .spawn(async move {
227 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
228 if empty_query
229 || queries
230 .iter()
231 .any(|query| query == ALL_TABS_COMPLETION_ITEM)
232 {
233 return Ok(open_buffers);
234 }
235
236 let matched_items = if strict_match {
237 let match_candidates = open_buffers
238 .iter()
239 .enumerate()
240 .filter_map(|(id, (full_path, ..))| {
241 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
242 Some((id, path_string))
243 })
244 .fold(HashMap::default(), |mut candidates, (id, path_string)| {
245 candidates
246 .entry(path_string)
247 .or_insert_with(|| Vec::new())
248 .push(id);
249 candidates
250 });
251
252 queries
253 .iter()
254 .filter_map(|query| match_candidates.get(query))
255 .flatten()
256 .copied()
257 .filter_map(|id| open_buffers.get(id))
258 .cloned()
259 .collect()
260 } else {
261 let match_candidates = open_buffers
262 .iter()
263 .enumerate()
264 .filter_map(|(id, (full_path, ..))| {
265 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
266 Some(fuzzy::StringMatchCandidate {
267 id,
268 char_bag: path_string.as_str().into(),
269 string: path_string,
270 })
271 })
272 .collect::<Vec<_>>();
273 let mut processed_matches = HashSet::default();
274 let file_queries = queries.iter().map(|query| {
275 fuzzy::match_strings(
276 &match_candidates,
277 query,
278 true,
279 usize::MAX,
280 &cancel,
281 background_executor.clone(),
282 )
283 });
284
285 join_all(file_queries)
286 .await
287 .into_iter()
288 .flatten()
289 .filter(|string_match| processed_matches.insert(string_match.candidate_id))
290 .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
291 .cloned()
292 .collect()
293 };
294 Ok(matched_items)
295 })
296 .await
297 })
298}
299
300fn active_item_buffer(
301 workspace: &mut Workspace,
302 cx: &mut ui::ViewContext<Workspace>,
303) -> anyhow::Result<BufferSnapshot> {
304 let active_editor = workspace
305 .active_item(cx)
306 .context("no active item")?
307 .downcast::<Editor>()
308 .context("active item is not an editor")?;
309 let snapshot = active_editor
310 .read(cx)
311 .buffer()
312 .read(cx)
313 .as_singleton()
314 .context("active editor is not a singleton buffer")?
315 .read(cx)
316 .snapshot();
317 Ok(snapshot)
318}