prompt_command.rs

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