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