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, CodeLabel, HighlightId, LspAdapterDelegate};
 13use std::{
 14    fmt::Write,
 15    path::PathBuf,
 16    sync::{atomic::AtomicBool, Arc},
 17};
 18use ui::{ActiveTheme, 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
 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        workspace: WeakView<Workspace>,
135        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
136        cx: &mut WindowContext,
137    ) -> Task<Result<SlashCommandOutput>> {
138        let tab_items_search = tab_items_for_queries(
139            Some(workspace),
140            arguments,
141            Arc::new(AtomicBool::new(false)),
142            true,
143            cx,
144        );
145
146        cx.background_executor().spawn(async move {
147            let mut sections = Vec::new();
148            let mut text = String::new();
149            let mut has_diagnostics = false;
150            for (full_path, buffer, _) in tab_items_search.await? {
151                let section_start_ix = text.len();
152                text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
153                for chunk in buffer.as_rope().chunks() {
154                    text.push_str(chunk);
155                }
156                if !text.ends_with('\n') {
157                    text.push('\n');
158                }
159                writeln!(text, "```").unwrap();
160                if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
161                    has_diagnostics = true;
162                }
163                if !text.ends_with('\n') {
164                    text.push('\n');
165                }
166
167                let section_end_ix = text.len() - 1;
168                sections.push(build_entry_output_section(
169                    section_start_ix..section_end_ix,
170                    full_path.as_deref(),
171                    false,
172                    None,
173                ));
174            }
175
176            Ok(SlashCommandOutput {
177                text,
178                sections,
179                run_commands_in_text: has_diagnostics,
180            })
181        })
182    }
183}
184
185fn tab_items_for_queries(
186    workspace: Option<WeakView<Workspace>>,
187    queries: &[String],
188    cancel: Arc<AtomicBool>,
189    strict_match: bool,
190    cx: &mut WindowContext,
191) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
192    let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
193    let queries = queries.to_owned();
194    cx.spawn(|mut cx| async move {
195        let mut open_buffers =
196            workspace
197                .context("no workspace")?
198                .update(&mut cx, |workspace, cx| {
199                    if strict_match && empty_query {
200                        let snapshot = active_item_buffer(workspace, cx)?;
201                        let full_path = snapshot.resolve_file_path(cx, true);
202                        return anyhow::Ok(vec![(full_path, snapshot, 0)]);
203                    }
204
205                    let mut timestamps_by_entity_id = HashMap::default();
206                    let mut visited_buffers = HashSet::default();
207                    let mut open_buffers = Vec::new();
208
209                    for pane in workspace.panes() {
210                        let pane = pane.read(cx);
211                        for entry in pane.activation_history() {
212                            timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
213                        }
214                    }
215
216                    for editor in workspace.items_of_type::<Editor>(cx) {
217                        if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
218                            if let Some(timestamp) =
219                                timestamps_by_entity_id.get(&editor.entity_id())
220                            {
221                                if visited_buffers.insert(buffer.read(cx).remote_id()) {
222                                    let snapshot = buffer.read(cx).snapshot();
223                                    let full_path = snapshot.resolve_file_path(cx, true);
224                                    open_buffers.push((full_path, snapshot, *timestamp));
225                                }
226                            }
227                        }
228                    }
229
230                    Ok(open_buffers)
231                })??;
232
233        let background_executor = cx.background_executor().clone();
234        cx.background_executor()
235            .spawn(async move {
236                open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
237                if empty_query
238                    || queries
239                        .iter()
240                        .any(|query| query == ALL_TABS_COMPLETION_ITEM)
241                {
242                    return Ok(open_buffers);
243                }
244
245                let matched_items = if strict_match {
246                    let match_candidates = open_buffers
247                        .iter()
248                        .enumerate()
249                        .filter_map(|(id, (full_path, ..))| {
250                            let path_string = full_path.as_deref()?.to_string_lossy().to_string();
251                            Some((id, path_string))
252                        })
253                        .fold(HashMap::default(), |mut candidates, (id, path_string)| {
254                            candidates
255                                .entry(path_string)
256                                .or_insert_with(Vec::new)
257                                .push(id);
258                            candidates
259                        });
260
261                    queries
262                        .iter()
263                        .filter_map(|query| match_candidates.get(query))
264                        .flatten()
265                        .copied()
266                        .filter_map(|id| open_buffers.get(id))
267                        .cloned()
268                        .collect()
269                } else {
270                    let match_candidates = open_buffers
271                        .iter()
272                        .enumerate()
273                        .filter_map(|(id, (full_path, ..))| {
274                            let path_string = full_path.as_deref()?.to_string_lossy().to_string();
275                            Some(fuzzy::StringMatchCandidate {
276                                id,
277                                char_bag: path_string.as_str().into(),
278                                string: path_string,
279                            })
280                        })
281                        .collect::<Vec<_>>();
282                    let mut processed_matches = HashSet::default();
283                    let file_queries = queries.iter().map(|query| {
284                        fuzzy::match_strings(
285                            &match_candidates,
286                            query,
287                            true,
288                            usize::MAX,
289                            &cancel,
290                            background_executor.clone(),
291                        )
292                    });
293
294                    join_all(file_queries)
295                        .await
296                        .into_iter()
297                        .flatten()
298                        .filter(|string_match| processed_matches.insert(string_match.candidate_id))
299                        .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
300                        .cloned()
301                        .collect()
302                };
303                Ok(matched_items)
304            })
305            .await
306    })
307}
308
309fn active_item_buffer(
310    workspace: &mut Workspace,
311    cx: &mut ui::ViewContext<Workspace>,
312) -> anyhow::Result<BufferSnapshot> {
313    let active_editor = workspace
314        .active_item(cx)
315        .context("no active item")?
316        .downcast::<Editor>()
317        .context("active item is not an editor")?;
318    let snapshot = active_editor
319        .read(cx)
320        .buffer()
321        .read(cx)
322        .as_singleton()
323        .context("active editor is not a singleton buffer")?
324        .read(cx)
325        .snapshot();
326    Ok(snapshot)
327}
328
329fn create_tab_completion_label(
330    path: &std::path::Path,
331    comment_id: Option<HighlightId>,
332) -> CodeLabel {
333    let file_name = path
334        .file_name()
335        .map(|f| f.to_string_lossy())
336        .unwrap_or_default();
337    let parent_path = path
338        .parent()
339        .map(|p| p.to_string_lossy())
340        .unwrap_or_default();
341    let mut label = CodeLabel::default();
342    label.push_str(&file_name, None);
343    label.push_str(" ", None);
344    label.push_str(&parent_path, comment_id);
345    label.filter_range = 0..file_name.len();
346    label
347}