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}