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