extension_slash_command.rs

  1use std::path::PathBuf;
  2use std::sync::{Arc, atomic::AtomicBool};
  3
  4use anyhow::Result;
  5use async_trait::async_trait;
  6use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
  7use gpui::{App, Task, WeakEntity, Window};
  8use language::{BufferSnapshot, LspAdapterDelegate};
  9use ui::prelude::*;
 10use workspace::Workspace;
 11
 12use crate::{
 13    ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
 14    SlashCommandRegistry, SlashCommandResult,
 15};
 16
 17pub fn init(cx: &mut App) {
 18    let proxy = ExtensionHostProxy::default_global(cx);
 19    proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
 20        slash_command_registry: SlashCommandRegistry::global(cx),
 21    });
 22}
 23
 24struct SlashCommandRegistryProxy {
 25    slash_command_registry: Arc<SlashCommandRegistry>,
 26}
 27
 28impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
 29    fn register_slash_command(
 30        &self,
 31        extension: Arc<dyn Extension>,
 32        command: extension::SlashCommand,
 33    ) {
 34        self.slash_command_registry
 35            .register_command(ExtensionSlashCommand::new(extension, command), false)
 36    }
 37
 38    fn unregister_slash_command(&self, command_name: Arc<str>) {
 39        self.slash_command_registry
 40            .unregister_command_by_name(&command_name)
 41    }
 42}
 43
 44/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
 45struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
 46
 47#[async_trait]
 48impl WorktreeDelegate for WorktreeDelegateAdapter {
 49    fn id(&self) -> u64 {
 50        self.0.worktree_id().to_proto()
 51    }
 52
 53    fn root_path(&self) -> String {
 54        self.0.worktree_root_path().to_string_lossy().to_string()
 55    }
 56
 57    async fn read_text_file(&self, path: PathBuf) -> Result<String> {
 58        self.0.read_text_file(path).await
 59    }
 60
 61    async fn which(&self, binary_name: String) -> Option<String> {
 62        self.0
 63            .which(binary_name.as_ref())
 64            .await
 65            .map(|path| path.to_string_lossy().to_string())
 66    }
 67
 68    async fn shell_env(&self) -> Vec<(String, String)> {
 69        self.0.shell_env().await.into_iter().collect()
 70    }
 71}
 72
 73pub struct ExtensionSlashCommand {
 74    extension: Arc<dyn Extension>,
 75    command: extension::SlashCommand,
 76}
 77
 78impl ExtensionSlashCommand {
 79    pub fn new(extension: Arc<dyn Extension>, command: extension::SlashCommand) -> Self {
 80        Self { extension, command }
 81    }
 82}
 83
 84impl SlashCommand for ExtensionSlashCommand {
 85    fn name(&self) -> String {
 86        self.command.name.clone()
 87    }
 88
 89    fn description(&self) -> String {
 90        self.command.description.clone()
 91    }
 92
 93    fn menu_text(&self) -> String {
 94        self.command.tooltip_text.clone()
 95    }
 96
 97    fn requires_argument(&self) -> bool {
 98        self.command.requires_argument
 99    }
100
101    fn complete_argument(
102        self: Arc<Self>,
103        arguments: &[String],
104        _cancel: Arc<AtomicBool>,
105        _workspace: Option<WeakEntity<Workspace>>,
106        _window: &mut Window,
107        cx: &mut App,
108    ) -> Task<Result<Vec<ArgumentCompletion>>> {
109        let command = self.command.clone();
110        let arguments = arguments.to_owned();
111        cx.background_spawn(async move {
112            let completions = self
113                .extension
114                .complete_slash_command_argument(command, arguments)
115                .await?;
116
117            anyhow::Ok(
118                completions
119                    .into_iter()
120                    .map(|completion| ArgumentCompletion {
121                        label: completion.label.into(),
122                        new_text: completion.new_text,
123                        replace_previous_arguments: false,
124                        after_completion: completion.run_command.into(),
125                    })
126                    .collect(),
127            )
128        })
129    }
130
131    fn run(
132        self: Arc<Self>,
133        arguments: &[String],
134        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
135        _context_buffer: BufferSnapshot,
136        _workspace: WeakEntity<Workspace>,
137        delegate: Option<Arc<dyn LspAdapterDelegate>>,
138        _window: &mut Window,
139        cx: &mut App,
140    ) -> Task<SlashCommandResult> {
141        let command = self.command.clone();
142        let arguments = arguments.to_owned();
143        let output = cx.background_spawn(async move {
144            let delegate =
145                delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _);
146            let output = self
147                .extension
148                .run_slash_command(command, arguments, delegate)
149                .await?;
150
151            anyhow::Ok(output)
152        });
153        cx.foreground_executor().spawn(async move {
154            let output = output.await?;
155            Ok(SlashCommandOutput {
156                text: output.text,
157                sections: output
158                    .sections
159                    .into_iter()
160                    .map(|section| SlashCommandOutputSection {
161                        range: section.range,
162                        icon: IconName::Code,
163                        label: section.label.into(),
164                        metadata: None,
165                    })
166                    .collect(),
167                run_commands_in_text: false,
168            }
169            .into_event_stream())
170        })
171    }
172}