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::{PromptMetadata, 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.spawn(async move |cx| {
47 let cancellation_flag = Arc::new(AtomicBool::default());
48 let prompts: Vec<PromptMetadata> = store
49 .await?
50 .read_with(cx, |store, cx| store.search(query, cancellation_flag, cx))?
51 .await;
52 Ok(prompts
53 .into_iter()
54 .filter_map(|prompt| {
55 let prompt_title = prompt.title?.to_string();
56 Some(ArgumentCompletion {
57 label: prompt_title.clone().into(),
58 new_text: prompt_title,
59 after_completion: true.into(),
60 replace_previous_arguments: true,
61 })
62 })
63 .collect())
64 })
65 }
66
67 fn run(
68 self: Arc<Self>,
69 arguments: &[String],
70 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
71 _context_buffer: BufferSnapshot,
72 _workspace: WeakEntity<Workspace>,
73 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
74 _: &mut Window,
75 cx: &mut App,
76 ) -> Task<SlashCommandResult> {
77 let title = arguments.to_owned().join(" ");
78 if title.trim().is_empty() {
79 return Task::ready(Err(anyhow!("missing prompt name")));
80 };
81
82 let store = PromptStore::global(cx);
83 let title = SharedString::from(title);
84 let prompt = cx.spawn({
85 let title = title.clone();
86 async move |cx| {
87 let store = store.await?;
88 let body = store
89 .read_with(cx, |store, cx| {
90 let prompt_id = store
91 .id_for_title(&title)
92 .with_context(|| format!("no prompt found with title {:?}", title))?;
93 anyhow::Ok(store.load(prompt_id, cx))
94 })??
95 .await?;
96 anyhow::Ok(body)
97 }
98 });
99 cx.foreground_executor().spawn(async move {
100 let mut prompt = prompt.await?;
101
102 if prompt.starts_with('/') {
103 // Prevent an edge case where the inserted prompt starts with a slash command (that leads to funky rendering).
104 prompt.insert(0, '\n');
105 }
106 if prompt.is_empty() {
107 prompt.push('\n');
108 }
109 let range = 0..prompt.len();
110 Ok(SlashCommandOutput {
111 text: prompt,
112 sections: vec![SlashCommandOutputSection {
113 range,
114 icon: IconName::Library,
115 label: title,
116 metadata: None,
117 }],
118 run_commands_in_text: true,
119 }
120 .into_event_stream())
121 })
122 }
123}