assistant_slash_command.rs

  1mod slash_command_registry;
  2
  3use anyhow::Result;
  4use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
  5use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
  6use serde::{Deserialize, Serialize};
  7pub use slash_command_registry::*;
  8use std::{
  9    ops::Range,
 10    sync::{atomic::AtomicBool, Arc},
 11};
 12use workspace::{ui::IconName, Workspace};
 13
 14pub fn init(cx: &mut AppContext) {
 15    SlashCommandRegistry::default_global(cx);
 16}
 17
 18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 19pub enum AfterCompletion {
 20    /// Run the command
 21    Run,
 22    /// Continue composing the current argument, doesn't add a space
 23    Compose,
 24    /// Continue the command composition, adds a space
 25    Continue,
 26}
 27
 28impl From<bool> for AfterCompletion {
 29    fn from(value: bool) -> Self {
 30        if value {
 31            AfterCompletion::Run
 32        } else {
 33            AfterCompletion::Continue
 34        }
 35    }
 36}
 37
 38impl AfterCompletion {
 39    pub fn run(&self) -> bool {
 40        match self {
 41            AfterCompletion::Run => true,
 42            AfterCompletion::Compose | AfterCompletion::Continue => false,
 43        }
 44    }
 45}
 46
 47#[derive(Debug)]
 48pub struct ArgumentCompletion {
 49    /// The label to display for this completion.
 50    pub label: CodeLabel,
 51    /// The new text that should be inserted into the command when this completion is accepted.
 52    pub new_text: String,
 53    /// Whether the command should be run when accepting this completion.
 54    pub after_completion: AfterCompletion,
 55    /// Whether to replace the all arguments, or whether to treat this as an independent argument.
 56    pub replace_previous_arguments: bool,
 57}
 58
 59pub type SlashCommandResult = Result<SlashCommandOutput>;
 60
 61pub trait SlashCommand: 'static + Send + Sync {
 62    fn name(&self) -> String;
 63    fn label(&self, _cx: &AppContext) -> CodeLabel {
 64        CodeLabel::plain(self.name(), None)
 65    }
 66    fn description(&self) -> String;
 67    fn menu_text(&self) -> String;
 68    fn complete_argument(
 69        self: Arc<Self>,
 70        arguments: &[String],
 71        cancel: Arc<AtomicBool>,
 72        workspace: Option<WeakView<Workspace>>,
 73        cx: &mut WindowContext,
 74    ) -> Task<Result<Vec<ArgumentCompletion>>>;
 75    fn requires_argument(&self) -> bool;
 76    fn accepts_arguments(&self) -> bool {
 77        self.requires_argument()
 78    }
 79    fn run(
 80        self: Arc<Self>,
 81        arguments: &[String],
 82        context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
 83        context_buffer: BufferSnapshot,
 84        workspace: WeakView<Workspace>,
 85        // TODO: We're just using the `LspAdapterDelegate` here because that is
 86        // what the extension API is already expecting.
 87        //
 88        // It may be that `LspAdapterDelegate` needs a more general name, or
 89        // perhaps another kind of delegate is needed here.
 90        delegate: Option<Arc<dyn LspAdapterDelegate>>,
 91        cx: &mut WindowContext,
 92    ) -> Task<SlashCommandResult>;
 93}
 94
 95pub type RenderFoldPlaceholder = Arc<
 96    dyn Send
 97        + Sync
 98        + Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
 99>;
100
101#[derive(Debug, Default, PartialEq)]
102pub struct SlashCommandOutput {
103    pub text: String,
104    pub sections: Vec<SlashCommandOutputSection<usize>>,
105    pub run_commands_in_text: bool,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109pub struct SlashCommandOutputSection<T> {
110    pub range: Range<T>,
111    pub icon: IconName,
112    pub label: SharedString,
113    pub metadata: Option<serde_json::Value>,
114}
115
116impl SlashCommandOutputSection<language::Anchor> {
117    pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
118        self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
119    }
120}