tab_command.rs

  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}