prompt_command.rs

  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}