1use super::{
2 diagnostics_command::write_single_file_diagnostics,
3 file_command::{build_entry_output_section, codeblock_fence_for_path},
4 SlashCommand, SlashCommandOutput,
5};
6use anyhow::{Context, Result};
7use assistant_slash_command::ArgumentCompletion;
8use collections::{HashMap, HashSet};
9use editor::Editor;
10use futures::future::join_all;
11use gpui::{Entity, Task, WeakView};
12use language::{BufferSnapshot, LspAdapterDelegate};
13use std::{
14 fmt::Write,
15 path::PathBuf,
16 sync::{atomic::AtomicBool, Arc},
17};
18use ui::WindowContext;
19use workspace::Workspace;
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 "Insert Open Tabs".to_owned()
36 }
37
38 fn requires_argument(&self) -> bool {
39 false
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 cx.spawn(|_| async move {
79 let tab_items = tab_items_search.await?;
80 let run_command = tab_items.len() == 1;
81 let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
82 let path_string = path.as_deref()?.to_string_lossy().to_string();
83 if argument_set.contains(&path_string) {
84 return None;
85 }
86 if active_item_path.is_some() && active_item_path == path {
87 return None;
88 }
89 Some(ArgumentCompletion {
90 label: path_string.clone().into(),
91 new_text: path_string,
92 replace_previous_arguments: false,
93 run_command,
94 })
95 });
96
97 let active_item_completion = active_item_path.as_deref().map(|active_item_path| {
98 let path_string = active_item_path.to_string_lossy().to_string();
99 ArgumentCompletion {
100 label: path_string.clone().into(),
101 new_text: path_string,
102 replace_previous_arguments: false,
103 run_command,
104 }
105 });
106
107 Ok(active_item_completion
108 .into_iter()
109 .chain(Some(ArgumentCompletion {
110 label: ALL_TABS_COMPLETION_ITEM.into(),
111 new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
112 replace_previous_arguments: false,
113 run_command: true,
114 }))
115 .chain(tab_completion_items)
116 .collect())
117 })
118 }
119
120 fn run(
121 self: Arc<Self>,
122 arguments: &[String],
123 workspace: WeakView<Workspace>,
124 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
125 cx: &mut WindowContext,
126 ) -> Task<Result<SlashCommandOutput>> {
127 let tab_items_search = tab_items_for_queries(
128 Some(workspace),
129 arguments,
130 Arc::new(AtomicBool::new(false)),
131 true,
132 cx,
133 );
134
135 cx.background_executor().spawn(async move {
136 let mut sections = Vec::new();
137 let mut text = String::new();
138 let mut has_diagnostics = false;
139 for (full_path, buffer, _) in tab_items_search.await? {
140 let section_start_ix = text.len();
141 text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
142 for chunk in buffer.as_rope().chunks() {
143 text.push_str(chunk);
144 }
145 if !text.ends_with('\n') {
146 text.push('\n');
147 }
148 writeln!(text, "```").unwrap();
149 if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
150 has_diagnostics = true;
151 }
152 if !text.ends_with('\n') {
153 text.push('\n');
154 }
155
156 let section_end_ix = text.len() - 1;
157 sections.push(build_entry_output_section(
158 section_start_ix..section_end_ix,
159 full_path.as_deref(),
160 false,
161 None,
162 ));
163 }
164
165 Ok(SlashCommandOutput {
166 text,
167 sections,
168 run_commands_in_text: has_diagnostics,
169 })
170 })
171 }
172}
173
174fn tab_items_for_queries(
175 workspace: Option<WeakView<Workspace>>,
176 queries: &[String],
177 cancel: Arc<AtomicBool>,
178 strict_match: bool,
179 cx: &mut WindowContext,
180) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
181 let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
182 let queries = queries.to_owned();
183 cx.spawn(|mut cx| async move {
184 let mut open_buffers =
185 workspace
186 .context("no workspace")?
187 .update(&mut cx, |workspace, cx| {
188 if strict_match && empty_query {
189 let snapshot = active_item_buffer(workspace, cx)?;
190 let full_path = snapshot.resolve_file_path(cx, true);
191 return anyhow::Ok(vec![(full_path, snapshot, 0)]);
192 }
193
194 let mut timestamps_by_entity_id = HashMap::default();
195 let mut open_buffers = Vec::new();
196
197 for pane in workspace.panes() {
198 let pane = pane.read(cx);
199 for entry in pane.activation_history() {
200 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
201 }
202 }
203
204 for editor in workspace.items_of_type::<Editor>(cx) {
205 if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
206 if let Some(timestamp) =
207 timestamps_by_entity_id.get(&editor.entity_id())
208 {
209 let snapshot = buffer.read(cx).snapshot();
210 let full_path = snapshot.resolve_file_path(cx, true);
211 open_buffers.push((full_path, snapshot, *timestamp));
212 }
213 }
214 }
215
216 Ok(open_buffers)
217 })??;
218
219 let background_executor = cx.background_executor().clone();
220 cx.background_executor()
221 .spawn(async move {
222 open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
223 if empty_query
224 || queries
225 .iter()
226 .any(|query| query == ALL_TABS_COMPLETION_ITEM)
227 {
228 return Ok(open_buffers);
229 }
230
231 let matched_items = if strict_match {
232 let match_candidates = open_buffers
233 .iter()
234 .enumerate()
235 .filter_map(|(id, (full_path, ..))| {
236 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
237 Some((id, path_string))
238 })
239 .fold(HashMap::default(), |mut candidates, (id, path_string)| {
240 candidates
241 .entry(path_string)
242 .or_insert_with(|| Vec::new())
243 .push(id);
244 candidates
245 });
246
247 queries
248 .iter()
249 .filter_map(|query| match_candidates.get(query))
250 .flatten()
251 .copied()
252 .filter_map(|id| open_buffers.get(id))
253 .cloned()
254 .collect()
255 } else {
256 let match_candidates = open_buffers
257 .iter()
258 .enumerate()
259 .filter_map(|(id, (full_path, ..))| {
260 let path_string = full_path.as_deref()?.to_string_lossy().to_string();
261 Some(fuzzy::StringMatchCandidate {
262 id,
263 char_bag: path_string.as_str().into(),
264 string: path_string,
265 })
266 })
267 .collect::<Vec<_>>();
268 let mut processed_matches = HashSet::default();
269 let file_queries = queries.iter().map(|query| {
270 fuzzy::match_strings(
271 &match_candidates,
272 query,
273 true,
274 usize::MAX,
275 &cancel,
276 background_executor.clone(),
277 )
278 });
279
280 join_all(file_queries)
281 .await
282 .into_iter()
283 .flatten()
284 .filter(|string_match| processed_matches.insert(string_match.candidate_id))
285 .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
286 .cloned()
287 .collect()
288 };
289 Ok(matched_items)
290 })
291 .await
292 })
293}
294
295fn active_item_buffer(
296 workspace: &mut Workspace,
297 cx: &mut ui::ViewContext<Workspace>,
298) -> anyhow::Result<BufferSnapshot> {
299 let active_editor = workspace
300 .active_item(cx)
301 .context("no active item")?
302 .downcast::<Editor>()
303 .context("active item is not an editor")?;
304 let snapshot = active_editor
305 .read(cx)
306 .buffer()
307 .read(cx)
308 .as_singleton()
309 .context("active editor is not a singleton buffer")?
310 .read(cx)
311 .snapshot();
312 Ok(snapshot)
313}