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::{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}