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}