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}