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