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::{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 menu_text(&self) -> String {
35 self.description()
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
83 let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
84 cx.spawn(|_| async move {
85 let tab_items = tab_items_search.await?;
86 let run_command = tab_items.len() == 1;
87 let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
88 let path_string = path.as_deref()?.to_string_lossy().to_string();
89 if argument_set.contains(&path_string) {
90 return None;
91 }
92 if active_item_path.is_some() && active_item_path == path {
93 return None;
94 }
95 let label = create_tab_completion_label(path.as_ref()?, comment_id);
96 Some(ArgumentCompletion {
97 label,
98 new_text: path_string,
99 replace_previous_arguments: false,
100 after_completion: run_command.into(),
101 })
102 });
103
104 let active_item_completion = active_item_path
105 .as_deref()
106 .map(|active_item_path| {
107 let path_string = active_item_path.to_string_lossy().to_string();
108 let label = create_tab_completion_label(active_item_path, comment_id);
109 ArgumentCompletion {
110 label,
111 new_text: path_string,
112 replace_previous_arguments: false,
113 after_completion: run_command.into(),
114 }
115 })
116 .filter(|completion| !argument_set.contains(&completion.new_text));
117
118 Ok(active_item_completion
119 .into_iter()
120 .chain(Some(ArgumentCompletion {
121 label: ALL_TABS_COMPLETION_ITEM.into(),
122 new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
123 replace_previous_arguments: false,
124 after_completion: true.into(),
125 }))
126 .chain(tab_completion_items)
127 .collect())
128 })
129 }
130
131 fn run(
132 self: Arc<Self>,
133 arguments: &[String],
134 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
135 _context_buffer: BufferSnapshot,
136 workspace: WeakView<Workspace>,
137 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
138 cx: &mut WindowContext,
139 ) -> Task<SlashCommandResult> {
140 let tab_items_search = tab_items_for_queries(
141 Some(workspace),
142 arguments,
143 Arc::new(AtomicBool::new(false)),
144 true,
145 cx,
146 );
147
148 cx.background_executor().spawn(async move {
149 let mut output = SlashCommandOutput::default();
150 for (full_path, buffer, _) in tab_items_search.await? {
151 append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
152 }
153 Ok(output.to_event_stream())
154 })
155 }
156}
157
158fn tab_items_for_queries(
159 workspace: Option<WeakView<Workspace>>,
160 queries: &[String],
161 cancel: Arc<AtomicBool>,
162 strict_match: bool,
163 cx: &mut WindowContext,
164) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
165 let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
166 let queries = queries.to_owned();
167 cx.spawn(|mut cx| async move {
168 let mut open_buffers =
169 workspace
170 .context("no workspace")?
171 .update(&mut cx, |workspace, cx| {
172 if strict_match && empty_query {
173 let snapshot = active_item_buffer(workspace, cx)?;
174 let full_path = snapshot.resolve_file_path(cx, true);
175 return anyhow::Ok(vec![(full_path, snapshot, 0)]);
176 }
177
178 let mut timestamps_by_entity_id = HashMap::default();
179 let mut visited_buffers = HashSet::default();
180 let mut open_buffers = Vec::new();
181
182 for pane in workspace.panes() {
183 let pane = pane.read(cx);
184 for entry in pane.activation_history() {
185 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
186 }
187 }
188
189 for editor in workspace.items_of_type::<Editor>(cx) {
190 if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
191 if let Some(timestamp) =
192 timestamps_by_entity_id.get(&editor.entity_id())
193 {
194 if visited_buffers.insert(buffer.read(cx).remote_id()) {
195 let snapshot = buffer.read(cx).snapshot();
196 let full_path = snapshot.resolve_file_path(cx, true);
197 open_buffers.push((full_path, snapshot, *timestamp));
198 }
199 }
200 }
201 }
202
203 Ok(open_buffers)
204 })??;
205
206 let background_executor = cx.background_executor().clone();
207 cx.background_executor()
208 .spawn(async move {
209 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
210 if empty_query
211 || queries
212 .iter()
213 .any(|query| query == ALL_TABS_COMPLETION_ITEM)
214 {
215 return Ok(open_buffers);
216 }
217
218 let matched_items = if strict_match {
219 let match_candidates = open_buffers
220 .iter()
221 .enumerate()
222 .filter_map(|(id, (full_path, ..))| {
223 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
224 Some((id, path_string))
225 })
226 .fold(HashMap::default(), |mut candidates, (id, path_string)| {
227 candidates
228 .entry(path_string)
229 .or_insert_with(Vec::new)
230 .push(id);
231 candidates
232 });
233
234 queries
235 .iter()
236 .filter_map(|query| match_candidates.get(query))
237 .flatten()
238 .copied()
239 .filter_map(|id| open_buffers.get(id))
240 .cloned()
241 .collect()
242 } else {
243 let match_candidates = open_buffers
244 .iter()
245 .enumerate()
246 .filter_map(|(id, (full_path, ..))| {
247 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
248 Some(fuzzy::StringMatchCandidate {
249 id,
250 char_bag: path_string.as_str().into(),
251 string: path_string,
252 })
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 ui::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}