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