current_file_command.rs

  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}