tab_command.rs

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