tabs_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;
  9use editor::Editor;
 10use gpui::{Entity, Task, WeakView};
 11use language::{BufferSnapshot, LspAdapterDelegate};
 12use std::{
 13    fmt::Write,
 14    path::PathBuf,
 15    sync::{atomic::AtomicBool, Arc},
 16};
 17use ui::WindowContext;
 18use workspace::Workspace;
 19
 20pub(crate) struct TabsSlashCommand;
 21
 22const ALL_TABS_COMPLETION_ITEM: &str = "all";
 23
 24impl SlashCommand for TabsSlashCommand {
 25    fn name(&self) -> String {
 26        "tabs".into()
 27    }
 28
 29    fn description(&self) -> String {
 30        "insert open tabs (active tab by default)".to_owned()
 31    }
 32
 33    fn menu_text(&self) -> String {
 34        "Insert Open Tabs".to_owned()
 35    }
 36
 37    fn requires_argument(&self) -> bool {
 38        false
 39    }
 40
 41    fn complete_argument(
 42        self: Arc<Self>,
 43        query: String,
 44        cancel: Arc<AtomicBool>,
 45        workspace: Option<WeakView<Workspace>>,
 46        cx: &mut WindowContext,
 47    ) -> Task<Result<Vec<ArgumentCompletion>>> {
 48        let all_tabs_completion_item = if ALL_TABS_COMPLETION_ITEM.contains(&query) {
 49            Some(ArgumentCompletion {
 50                label: ALL_TABS_COMPLETION_ITEM.to_owned(),
 51                new_text: ALL_TABS_COMPLETION_ITEM.to_owned(),
 52                run_command: true,
 53            })
 54        } else {
 55            None
 56        };
 57        let tab_items_search = tab_items_for_query(workspace, query, cancel, false, cx);
 58        cx.spawn(|_| async move {
 59            let tab_completion_items =
 60                tab_items_search
 61                    .await?
 62                    .into_iter()
 63                    .filter_map(|(path, ..)| {
 64                        let path_string = path.as_deref()?.to_string_lossy().to_string();
 65                        Some(ArgumentCompletion {
 66                            label: path_string.clone(),
 67                            new_text: path_string,
 68                            run_command: true,
 69                        })
 70                    });
 71            Ok(all_tabs_completion_item
 72                .into_iter()
 73                .chain(tab_completion_items)
 74                .collect::<Vec<_>>())
 75        })
 76    }
 77
 78    fn run(
 79        self: Arc<Self>,
 80        argument: Option<&str>,
 81        workspace: WeakView<Workspace>,
 82        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
 83        cx: &mut WindowContext,
 84    ) -> Task<Result<SlashCommandOutput>> {
 85        let tab_items_search = tab_items_for_query(
 86            Some(workspace),
 87            argument.map(ToOwned::to_owned).unwrap_or_default(),
 88            Arc::new(AtomicBool::new(false)),
 89            true,
 90            cx,
 91        );
 92
 93        cx.background_executor().spawn(async move {
 94            let mut sections = Vec::new();
 95            let mut text = String::new();
 96            let mut has_diagnostics = false;
 97            for (full_path, buffer, _) in tab_items_search.await? {
 98                let section_start_ix = text.len();
 99                text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
100                for chunk in buffer.as_rope().chunks() {
101                    text.push_str(chunk);
102                }
103                if !text.ends_with('\n') {
104                    text.push('\n');
105                }
106                writeln!(text, "```").unwrap();
107                if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
108                    has_diagnostics = true;
109                }
110                if !text.ends_with('\n') {
111                    text.push('\n');
112                }
113
114                let section_end_ix = text.len() - 1;
115                sections.push(build_entry_output_section(
116                    section_start_ix..section_end_ix,
117                    full_path.as_deref(),
118                    false,
119                    None,
120                ));
121            }
122
123            Ok(SlashCommandOutput {
124                text,
125                sections,
126                run_commands_in_text: has_diagnostics,
127            })
128        })
129    }
130}
131
132fn tab_items_for_query(
133    workspace: Option<WeakView<Workspace>>,
134    mut query: String,
135    cancel: Arc<AtomicBool>,
136    use_active_tab_for_empty_query: bool,
137    cx: &mut WindowContext,
138) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
139    cx.spawn(|mut cx| async move {
140        query.make_ascii_lowercase();
141        let mut open_buffers =
142            workspace
143                .context("no workspace")?
144                .update(&mut cx, |workspace, cx| {
145                    if use_active_tab_for_empty_query && query.trim().is_empty() {
146                        let active_editor = workspace
147                            .active_item(cx)
148                            .context("no active item")?
149                            .downcast::<Editor>()
150                            .context("active item is not an editor")?;
151                        let snapshot = active_editor
152                            .read(cx)
153                            .buffer()
154                            .read(cx)
155                            .as_singleton()
156                            .context("active editor is not a singleton buffer")?
157                            .read(cx)
158                            .snapshot();
159                        let full_path = snapshot.resolve_file_path(cx, true);
160                        return anyhow::Ok(vec![(full_path, snapshot, 0)]);
161                    }
162
163                    let mut timestamps_by_entity_id = HashMap::default();
164                    let mut open_buffers = Vec::new();
165
166                    for pane in workspace.panes() {
167                        let pane = pane.read(cx);
168                        for entry in pane.activation_history() {
169                            timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
170                        }
171                    }
172
173                    for editor in workspace.items_of_type::<Editor>(cx) {
174                        if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
175                            if let Some(timestamp) =
176                                timestamps_by_entity_id.get(&editor.entity_id())
177                            {
178                                let snapshot = buffer.read(cx).snapshot();
179                                let full_path = snapshot.resolve_file_path(cx, true);
180                                open_buffers.push((full_path, snapshot, *timestamp));
181                            }
182                        }
183                    }
184
185                    Ok(open_buffers)
186                })??;
187
188        let background_executor = cx.background_executor().clone();
189        cx.background_executor()
190            .spawn(async move {
191                open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
192                let query = query.trim();
193                if query.is_empty() || query == ALL_TABS_COMPLETION_ITEM {
194                    return Ok(open_buffers);
195                }
196
197                let match_candidates = open_buffers
198                    .iter()
199                    .enumerate()
200                    .filter_map(|(id, (full_path, ..))| {
201                        let path_string = full_path.as_deref()?.to_string_lossy().to_string();
202                        Some(fuzzy::StringMatchCandidate {
203                            id,
204                            char_bag: path_string.as_str().into(),
205                            string: path_string,
206                        })
207                    })
208                    .collect::<Vec<_>>();
209                let string_matches = fuzzy::match_strings(
210                    &match_candidates,
211                    &query,
212                    true,
213                    usize::MAX,
214                    &cancel,
215                    background_executor,
216                )
217                .await;
218
219                Ok(string_matches
220                    .into_iter()
221                    .filter_map(|string_match| open_buffers.get(string_match.candidate_id))
222                    .cloned()
223                    .collect())
224            })
225            .await
226    })
227}