1use anyhow::{Context as _, Result, anyhow};
2use assistant_slash_command::{
3 ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
4 SlashCommandResult,
5};
6use gpui::{Task, WeakEntity};
7use language::{BufferSnapshot, LspAdapterDelegate};
8use prompt_store::PromptStore;
9use std::sync::{Arc, atomic::AtomicBool};
10use ui::prelude::*;
11use workspace::Workspace;
12
13pub struct PromptSlashCommand;
14
15impl SlashCommand for PromptSlashCommand {
16 fn name(&self) -> String {
17 "prompt".into()
18 }
19
20 fn description(&self) -> String {
21 "Insert prompt from library".into()
22 }
23
24 fn icon(&self) -> IconName {
25 IconName::Library
26 }
27
28 fn menu_text(&self) -> String {
29 self.description()
30 }
31
32 fn requires_argument(&self) -> bool {
33 true
34 }
35
36 fn complete_argument(
37 self: Arc<Self>,
38 arguments: &[String],
39 _cancellation_flag: Arc<AtomicBool>,
40 _workspace: Option<WeakEntity<Workspace>>,
41 _: &mut Window,
42 cx: &mut App,
43 ) -> Task<Result<Vec<ArgumentCompletion>>> {
44 let store = PromptStore::global(cx);
45 let query = arguments.to_owned().join(" ");
46 cx.background_spawn(async move {
47 let prompts = store.await?.search(query).await;
48 Ok(prompts
49 .into_iter()
50 .filter_map(|prompt| {
51 let prompt_title = prompt.title?.to_string();
52 Some(ArgumentCompletion {
53 label: prompt_title.clone().into(),
54 new_text: prompt_title,
55 after_completion: true.into(),
56 replace_previous_arguments: true,
57 })
58 })
59 .collect())
60 })
61 }
62
63 fn run(
64 self: Arc<Self>,
65 arguments: &[String],
66 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
67 _context_buffer: BufferSnapshot,
68 _workspace: WeakEntity<Workspace>,
69 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
70 _: &mut Window,
71 cx: &mut App,
72 ) -> Task<SlashCommandResult> {
73 let title = arguments.to_owned().join(" ");
74 if title.trim().is_empty() {
75 return Task::ready(Err(anyhow!("missing prompt name")));
76 };
77
78 let store = PromptStore::global(cx);
79 let title = SharedString::from(title.clone());
80 let prompt = cx.background_spawn({
81 let title = title.clone();
82 async move {
83 let store = store.await?;
84 let prompt_id = store
85 .id_for_title(&title)
86 .with_context(|| format!("no prompt found with title {:?}", title))?;
87 let body = store.load(prompt_id).await?;
88 anyhow::Ok(body)
89 }
90 });
91 cx.foreground_executor().spawn(async move {
92 let mut prompt = prompt.await?;
93
94 if prompt.starts_with('/') {
95 // Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
96 prompt.insert(0, '\n');
97 }
98 if prompt.is_empty() {
99 prompt.push('\n');
100 }
101 let range = 0..prompt.len();
102 Ok(SlashCommandOutput {
103 text: prompt,
104 sections: vec![SlashCommandOutputSection {
105 range,
106 icon: IconName::Library,
107 label: title,
108 metadata: None,
109 }],
110 run_commands_in_text: true,
111 }
112 .to_event_stream())
113 })
114 }
115}