1use std::sync::Arc;
2use std::{borrow::Cow, cell::Cell, rc::Rc};
3
4use anyhow::{anyhow, Result};
5use collections::HashMap;
6use editor::Editor;
7use futures::channel::oneshot;
8use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
9use language::LspAdapterDelegate;
10use workspace::{Event as WorkspaceEvent, Workspace};
11
12use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
13
14pub(crate) struct CurrentFileSlashCommand {
15 workspace: WindowHandle<Workspace>,
16}
17
18impl CurrentFileSlashCommand {
19 pub fn new(workspace: WindowHandle<Workspace>) -> Self {
20 Self { workspace }
21 }
22}
23
24impl SlashCommand for CurrentFileSlashCommand {
25 fn name(&self) -> String {
26 "current_file".into()
27 }
28
29 fn description(&self) -> String {
30 "insert the current file".into()
31 }
32
33 fn complete_argument(
34 &self,
35 _query: String,
36 _cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
37 _cx: &mut AppContext,
38 ) -> Task<Result<Vec<String>>> {
39 Task::ready(Err(anyhow!("this command does not require argument")))
40 }
41
42 fn requires_argument(&self) -> bool {
43 false
44 }
45
46 fn run(
47 self: Arc<Self>,
48 _argument: Option<&str>,
49 _delegate: Arc<dyn LspAdapterDelegate>,
50 cx: &mut AppContext,
51 ) -> SlashCommandInvocation {
52 let (invalidate_tx, invalidate_rx) = oneshot::channel();
53 let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
54 let mut subscriptions: Vec<Subscription> = Vec::new();
55 let output = self.workspace.update(cx, |workspace, cx| {
56 let mut timestamps_by_entity_id = HashMap::default();
57 for pane in workspace.panes() {
58 let pane = pane.read(cx);
59 for entry in pane.activation_history() {
60 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
61 }
62 }
63
64 let mut most_recent_buffer = None;
65 for editor in workspace.items_of_type::<Editor>(cx) {
66 let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
67 continue;
68 };
69
70 let timestamp = timestamps_by_entity_id
71 .get(&editor.entity_id())
72 .copied()
73 .unwrap_or_default();
74 if most_recent_buffer
75 .as_ref()
76 .map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
77 {
78 most_recent_buffer = Some((buffer, timestamp));
79 }
80 }
81
82 subscriptions.push({
83 let workspace_view = cx.view().clone();
84 let invalidate_tx = invalidate_tx.clone();
85 cx.window_context()
86 .subscribe(&workspace_view, move |_workspace, event, _cx| match event {
87 WorkspaceEvent::ActiveItemChanged
88 | WorkspaceEvent::ItemAdded
89 | WorkspaceEvent::ItemRemoved
90 | WorkspaceEvent::PaneAdded(_)
91 | WorkspaceEvent::PaneRemoved => {
92 if let Some(invalidate_tx) = invalidate_tx.take() {
93 _ = invalidate_tx.send(());
94 }
95 }
96 _ => {}
97 })
98 });
99
100 if let Some((buffer, _)) = most_recent_buffer {
101 subscriptions.push({
102 let invalidate_tx = invalidate_tx.clone();
103 cx.window_context().observe(&buffer, move |_buffer, _cx| {
104 if let Some(invalidate_tx) = invalidate_tx.take() {
105 _ = invalidate_tx.send(());
106 }
107 })
108 });
109
110 let snapshot = buffer.read(cx).snapshot();
111 let path = snapshot.resolve_file_path(cx, true);
112 cx.background_executor().spawn(async move {
113 let path = path
114 .as_ref()
115 .map(|path| path.to_string_lossy())
116 .unwrap_or_else(|| Cow::Borrowed("untitled"));
117
118 let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
119 output.push_str("```");
120 output.push_str(&path);
121 output.push('\n');
122 for chunk in snapshot.as_rope().chunks() {
123 output.push_str(chunk);
124 }
125 if !output.ends_with('\n') {
126 output.push('\n');
127 }
128 output.push_str("```");
129 Ok(output)
130 })
131 } else {
132 Task::ready(Err(anyhow!("no recent buffer found")))
133 }
134 });
135
136 SlashCommandInvocation {
137 output: output.unwrap_or_else(|error| Task::ready(Err(error))),
138 invalidated: invalidate_rx,
139 cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
140 }
141 }
142}