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