tab_command.rs

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