1use std::path::PathBuf;
2use std::sync::{atomic::AtomicBool, Arc};
3
4use anyhow::Result;
5use async_trait::async_trait;
6use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
7use gpui::{AppContext, Task, WeakView, WindowContext};
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 AppContext) {
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
39/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
40struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
41
42#[async_trait]
43impl WorktreeDelegate for WorktreeDelegateAdapter {
44 fn id(&self) -> u64 {
45 self.0.worktree_id().to_proto()
46 }
47
48 fn root_path(&self) -> String {
49 self.0.worktree_root_path().to_string_lossy().to_string()
50 }
51
52 async fn read_text_file(&self, path: PathBuf) -> Result<String> {
53 self.0.read_text_file(path).await
54 }
55
56 async fn which(&self, binary_name: String) -> Option<String> {
57 self.0
58 .which(binary_name.as_ref())
59 .await
60 .map(|path| path.to_string_lossy().to_string())
61 }
62
63 async fn shell_env(&self) -> Vec<(String, String)> {
64 self.0.shell_env().await.into_iter().collect()
65 }
66}
67
68pub struct ExtensionSlashCommand {
69 extension: Arc<dyn Extension>,
70 command: extension::SlashCommand,
71}
72
73impl ExtensionSlashCommand {
74 pub fn new(extension: Arc<dyn Extension>, command: extension::SlashCommand) -> Self {
75 Self { extension, command }
76 }
77}
78
79impl SlashCommand for ExtensionSlashCommand {
80 fn name(&self) -> String {
81 self.command.name.clone()
82 }
83
84 fn description(&self) -> String {
85 self.command.description.clone()
86 }
87
88 fn menu_text(&self) -> String {
89 self.command.tooltip_text.clone()
90 }
91
92 fn requires_argument(&self) -> bool {
93 self.command.requires_argument
94 }
95
96 fn complete_argument(
97 self: Arc<Self>,
98 arguments: &[String],
99 _cancel: Arc<AtomicBool>,
100 _workspace: Option<WeakView<Workspace>>,
101 cx: &mut WindowContext,
102 ) -> Task<Result<Vec<ArgumentCompletion>>> {
103 let command = self.command.clone();
104 let arguments = arguments.to_owned();
105 cx.background_executor().spawn(async move {
106 let completions = self
107 .extension
108 .complete_slash_command_argument(command, arguments)
109 .await?;
110
111 anyhow::Ok(
112 completions
113 .into_iter()
114 .map(|completion| ArgumentCompletion {
115 label: completion.label.into(),
116 new_text: completion.new_text,
117 replace_previous_arguments: false,
118 after_completion: completion.run_command.into(),
119 })
120 .collect(),
121 )
122 })
123 }
124
125 fn run(
126 self: Arc<Self>,
127 arguments: &[String],
128 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
129 _context_buffer: BufferSnapshot,
130 _workspace: WeakView<Workspace>,
131 delegate: Option<Arc<dyn LspAdapterDelegate>>,
132 cx: &mut WindowContext,
133 ) -> Task<SlashCommandResult> {
134 let command = self.command.clone();
135 let arguments = arguments.to_owned();
136 let output = cx.background_executor().spawn(async move {
137 let delegate =
138 delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _);
139 let output = self
140 .extension
141 .run_slash_command(command, arguments, delegate)
142 .await?;
143
144 anyhow::Ok(output)
145 });
146 cx.foreground_executor().spawn(async move {
147 let output = output.await?;
148 Ok(SlashCommandOutput {
149 text: output.text,
150 sections: output
151 .sections
152 .into_iter()
153 .map(|section| SlashCommandOutputSection {
154 range: section.range,
155 icon: IconName::Code,
156 label: section.label.into(),
157 metadata: None,
158 })
159 .collect(),
160 run_commands_in_text: false,
161 }
162 .to_event_stream())
163 })
164 }
165}