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::Result;
  7use assistant_slash_command::ArgumentCompletion;
  8use collections::HashMap;
  9use editor::Editor;
 10use gpui::{AppContext, Entity, Task, WeakView};
 11use language::LspAdapterDelegate;
 12use std::{fmt::Write, sync::Arc};
 13use ui::WindowContext;
 14use workspace::Workspace;
 15
 16pub(crate) struct TabsSlashCommand;
 17
 18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 19enum TabsArgument {
 20    #[default]
 21    Active,
 22    All,
 23}
 24
 25impl TabsArgument {
 26    fn for_query(mut query: String) -> Vec<Self> {
 27        query.make_ascii_lowercase();
 28        let query = query.trim();
 29
 30        let mut matches = Vec::new();
 31        if Self::Active.name().contains(&query) {
 32            matches.push(Self::Active);
 33        }
 34        if Self::All.name().contains(&query) {
 35            matches.push(Self::All);
 36        }
 37        matches
 38    }
 39
 40    fn name(&self) -> &'static str {
 41        match self {
 42            Self::Active => "active",
 43            Self::All => "all",
 44        }
 45    }
 46
 47    fn from_name(name: &str) -> Option<Self> {
 48        match name {
 49            "active" => Some(Self::Active),
 50            "all" => Some(Self::All),
 51            _ => None,
 52        }
 53    }
 54}
 55
 56impl SlashCommand for TabsSlashCommand {
 57    fn name(&self) -> String {
 58        "tabs".into()
 59    }
 60
 61    fn description(&self) -> String {
 62        "insert open tabs".into()
 63    }
 64
 65    fn menu_text(&self) -> String {
 66        "Insert Open Tabs".into()
 67    }
 68
 69    fn requires_argument(&self) -> bool {
 70        true
 71    }
 72
 73    fn complete_argument(
 74        self: Arc<Self>,
 75        query: String,
 76        _cancel: Arc<std::sync::atomic::AtomicBool>,
 77        _workspace: Option<WeakView<Workspace>>,
 78        _cx: &mut AppContext,
 79    ) -> Task<Result<Vec<ArgumentCompletion>>> {
 80        let arguments = TabsArgument::for_query(query);
 81        Task::ready(Ok(arguments
 82            .into_iter()
 83            .map(|arg| ArgumentCompletion {
 84                label: arg.name().to_owned(),
 85                new_text: arg.name().to_owned(),
 86                run_command: true,
 87            })
 88            .collect()))
 89    }
 90
 91    fn run(
 92        self: Arc<Self>,
 93        argument: Option<&str>,
 94        workspace: WeakView<Workspace>,
 95        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
 96        cx: &mut WindowContext,
 97    ) -> Task<Result<SlashCommandOutput>> {
 98        let argument = argument
 99            .and_then(TabsArgument::from_name)
100            .unwrap_or_default();
101        let open_buffers = workspace.update(cx, |workspace, cx| match argument {
102            TabsArgument::Active => {
103                let Some(active_item) = workspace.active_item(cx) else {
104                    anyhow::bail!("no active item")
105                };
106                let Some(buffer) = active_item
107                    .downcast::<Editor>()
108                    .and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
109                else {
110                    anyhow::bail!("active item is not an editor")
111                };
112                let snapshot = buffer.read(cx).snapshot();
113                let full_path = snapshot.resolve_file_path(cx, true);
114                anyhow::Ok(vec![(full_path, snapshot, 0)])
115            }
116            TabsArgument::All => {
117                let mut timestamps_by_entity_id = HashMap::default();
118                let mut open_buffers = Vec::new();
119
120                for pane in workspace.panes() {
121                    let pane = pane.read(cx);
122                    for entry in pane.activation_history() {
123                        timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
124                    }
125                }
126
127                for editor in workspace.items_of_type::<Editor>(cx) {
128                    if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
129                        if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
130                            let snapshot = buffer.read(cx).snapshot();
131                            let full_path = snapshot.resolve_file_path(cx, true);
132                            open_buffers.push((full_path, snapshot, *timestamp));
133                        }
134                    }
135                }
136
137                Ok(open_buffers)
138            }
139        });
140
141        match open_buffers {
142            Ok(Ok(mut open_buffers)) => cx.background_executor().spawn(async move {
143                open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
144
145                let mut sections = Vec::new();
146                let mut text = String::new();
147                let mut has_diagnostics = false;
148                for (full_path, buffer, _) in open_buffers {
149                    let section_start_ix = text.len();
150                    text.push_str(&codeblock_fence_for_path(full_path.as_deref(), None));
151                    for chunk in buffer.as_rope().chunks() {
152                        text.push_str(chunk);
153                    }
154                    if !text.ends_with('\n') {
155                        text.push('\n');
156                    }
157                    writeln!(text, "```").unwrap();
158                    if write_single_file_diagnostics(&mut text, full_path.as_deref(), &buffer) {
159                        has_diagnostics = true;
160                    }
161                    if !text.ends_with('\n') {
162                        text.push('\n');
163                    }
164
165                    let section_end_ix = text.len() - 1;
166                    sections.push(build_entry_output_section(
167                        section_start_ix..section_end_ix,
168                        full_path.as_deref(),
169                        false,
170                        None,
171                    ));
172                }
173
174                Ok(SlashCommandOutput {
175                    text,
176                    sections,
177                    run_commands_in_text: has_diagnostics,
178                })
179            }),
180            Ok(Err(error)) | Err(error) => Task::ready(Err(error)),
181        }
182    }
183}