1mod extension_slash_command;
  2mod slash_command_registry;
  3mod slash_command_working_set;
  4
  5pub use crate::extension_slash_command::*;
  6pub use crate::slash_command_registry::*;
  7pub use crate::slash_command_working_set::*;
  8use anyhow::Result;
  9use futures::StreamExt;
 10use futures::stream::{self, BoxStream};
 11use gpui::{App, SharedString, Task, WeakEntity, Window};
 12use language::CodeLabelBuilder;
 13use language::HighlightId;
 14use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
 15pub use language_model::Role;
 16use serde::{Deserialize, Serialize};
 17use std::{
 18    ops::Range,
 19    sync::{Arc, atomic::AtomicBool},
 20};
 21use ui::ActiveTheme;
 22use workspace::{Workspace, ui::IconName};
 23
 24pub fn init(cx: &mut App) {
 25    SlashCommandRegistry::default_global(cx);
 26    extension_slash_command::init(cx);
 27}
 28
 29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 30pub enum AfterCompletion {
 31    /// Run the command
 32    Run,
 33    /// Continue composing the current argument, doesn't add a space
 34    Compose,
 35    /// Continue the command composition, adds a space
 36    Continue,
 37}
 38
 39impl From<bool> for AfterCompletion {
 40    fn from(value: bool) -> Self {
 41        if value {
 42            AfterCompletion::Run
 43        } else {
 44            AfterCompletion::Continue
 45        }
 46    }
 47}
 48
 49impl AfterCompletion {
 50    pub fn run(&self) -> bool {
 51        match self {
 52            AfterCompletion::Run => true,
 53            AfterCompletion::Compose | AfterCompletion::Continue => false,
 54        }
 55    }
 56}
 57
 58#[derive(Debug)]
 59pub struct ArgumentCompletion {
 60    /// The label to display for this completion.
 61    pub label: CodeLabel,
 62    /// The new text that should be inserted into the command when this completion is accepted.
 63    pub new_text: String,
 64    /// Whether the command should be run when accepting this completion.
 65    pub after_completion: AfterCompletion,
 66    /// Whether to replace the all arguments, or whether to treat this as an independent argument.
 67    pub replace_previous_arguments: bool,
 68}
 69
 70pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent>>>;
 71
 72pub trait SlashCommand: 'static + Send + Sync {
 73    fn name(&self) -> String;
 74    fn icon(&self) -> IconName {
 75        IconName::Slash
 76    }
 77    fn label(&self, _cx: &App) -> CodeLabel {
 78        CodeLabel::plain(self.name(), None)
 79    }
 80    fn description(&self) -> String;
 81    fn menu_text(&self) -> String;
 82    fn complete_argument(
 83        self: Arc<Self>,
 84        arguments: &[String],
 85        cancel: Arc<AtomicBool>,
 86        workspace: Option<WeakEntity<Workspace>>,
 87        window: &mut Window,
 88        cx: &mut App,
 89    ) -> Task<Result<Vec<ArgumentCompletion>>>;
 90    fn requires_argument(&self) -> bool;
 91    fn accepts_arguments(&self) -> bool {
 92        self.requires_argument()
 93    }
 94    fn run(
 95        self: Arc<Self>,
 96        arguments: &[String],
 97        context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
 98        context_buffer: BufferSnapshot,
 99        workspace: WeakEntity<Workspace>,
100        // TODO: We're just using the `LspAdapterDelegate` here because that is
101        // what the extension API is already expecting.
102        //
103        // It may be that `LspAdapterDelegate` needs a more general name, or
104        // perhaps another kind of delegate is needed here.
105        delegate: Option<Arc<dyn LspAdapterDelegate>>,
106        window: &mut Window,
107        cx: &mut App,
108    ) -> Task<SlashCommandResult>;
109}
110
111#[derive(Debug, PartialEq)]
112pub enum SlashCommandContent {
113    Text {
114        text: String,
115        run_commands_in_text: bool,
116    },
117}
118
119impl<'a> From<&'a str> for SlashCommandContent {
120    fn from(text: &'a str) -> Self {
121        Self::Text {
122            text: text.into(),
123            run_commands_in_text: false,
124        }
125    }
126}
127
128#[derive(Debug, PartialEq)]
129pub enum SlashCommandEvent {
130    StartMessage {
131        role: Role,
132        merge_same_roles: bool,
133    },
134    StartSection {
135        icon: IconName,
136        label: SharedString,
137        metadata: Option<serde_json::Value>,
138    },
139    Content(SlashCommandContent),
140    EndSection,
141}
142
143#[derive(Debug, Default, PartialEq, Clone)]
144pub struct SlashCommandOutput {
145    pub text: String,
146    pub sections: Vec<SlashCommandOutputSection<usize>>,
147    pub run_commands_in_text: bool,
148}
149
150impl SlashCommandOutput {
151    pub fn ensure_valid_section_ranges(&mut self) {
152        for section in &mut self.sections {
153            section.range.start = section.range.start.min(self.text.len());
154            section.range.end = section.range.end.min(self.text.len());
155            while !self.text.is_char_boundary(section.range.start) {
156                section.range.start -= 1;
157            }
158            while !self.text.is_char_boundary(section.range.end) {
159                section.range.end += 1;
160            }
161        }
162    }
163
164    /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
165    pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
166        self.ensure_valid_section_ranges();
167
168        let mut events = Vec::new();
169
170        let mut section_endpoints = Vec::new();
171        for section in self.sections {
172            section_endpoints.push((
173                section.range.start,
174                SlashCommandEvent::StartSection {
175                    icon: section.icon,
176                    label: section.label,
177                    metadata: section.metadata,
178                },
179            ));
180            section_endpoints.push((section.range.end, SlashCommandEvent::EndSection));
181        }
182        section_endpoints.sort_by_key(|(offset, _)| *offset);
183
184        let mut content_offset = 0;
185        for (endpoint_offset, endpoint) in section_endpoints {
186            if content_offset < endpoint_offset {
187                events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
188                    text: self.text[content_offset..endpoint_offset].to_string(),
189                    run_commands_in_text: self.run_commands_in_text,
190                })));
191                content_offset = endpoint_offset;
192            }
193
194            events.push(Ok(endpoint));
195        }
196
197        if content_offset < self.text.len() {
198            events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
199                text: self.text[content_offset..].to_string(),
200                run_commands_in_text: self.run_commands_in_text,
201            })));
202        }
203
204        stream::iter(events).boxed()
205    }
206
207    pub async fn from_event_stream(
208        mut events: BoxStream<'static, Result<SlashCommandEvent>>,
209    ) -> Result<SlashCommandOutput> {
210        let mut output = SlashCommandOutput::default();
211        let mut section_stack = Vec::new();
212
213        while let Some(event) = events.next().await {
214            match event? {
215                SlashCommandEvent::StartSection {
216                    icon,
217                    label,
218                    metadata,
219                } => {
220                    let start = output.text.len();
221                    section_stack.push(SlashCommandOutputSection {
222                        range: start..start,
223                        icon,
224                        label,
225                        metadata,
226                    });
227                }
228                SlashCommandEvent::Content(SlashCommandContent::Text {
229                    text,
230                    run_commands_in_text,
231                }) => {
232                    output.text.push_str(&text);
233                    output.run_commands_in_text = run_commands_in_text;
234
235                    if let Some(section) = section_stack.last_mut() {
236                        section.range.end = output.text.len();
237                    }
238                }
239                SlashCommandEvent::EndSection => {
240                    if let Some(section) = section_stack.pop() {
241                        output.sections.push(section);
242                    }
243                }
244                SlashCommandEvent::StartMessage { .. } => {}
245            }
246        }
247
248        while let Some(section) = section_stack.pop() {
249            output.sections.push(section);
250        }
251
252        Ok(output)
253    }
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257pub struct SlashCommandOutputSection<T> {
258    pub range: Range<T>,
259    pub icon: IconName,
260    pub label: SharedString,
261    pub metadata: Option<serde_json::Value>,
262}
263
264impl SlashCommandOutputSection<language::Anchor> {
265    pub fn is_valid(&self, buffer: &language::TextBuffer) -> bool {
266        self.range.start.is_valid(buffer) && !self.range.to_offset(buffer).is_empty()
267    }
268}
269
270pub struct SlashCommandLine {
271    /// The range within the line containing the command name.
272    pub name: Range<usize>,
273    /// Ranges within the line containing the command arguments.
274    pub arguments: Vec<Range<usize>>,
275}
276
277impl SlashCommandLine {
278    pub fn parse(line: &str) -> Option<Self> {
279        let mut call: Option<Self> = None;
280        let mut ix = 0;
281        for c in line.chars() {
282            let next_ix = ix + c.len_utf8();
283            if let Some(call) = &mut call {
284                // The command arguments start at the first non-whitespace character
285                // after the command name, and continue until the end of the line.
286                if let Some(argument) = call.arguments.last_mut() {
287                    if c.is_whitespace() {
288                        if (*argument).is_empty() {
289                            argument.start = next_ix;
290                            argument.end = next_ix;
291                        } else {
292                            argument.end = ix;
293                            call.arguments.push(next_ix..next_ix);
294                        }
295                    } else {
296                        argument.end = next_ix;
297                    }
298                }
299                // The command name ends at the first whitespace character.
300                else if !call.name.is_empty() {
301                    if c.is_whitespace() {
302                        call.arguments = vec![next_ix..next_ix];
303                    } else {
304                        call.name.end = next_ix;
305                    }
306                }
307                // The command name must begin with a letter.
308                else if c.is_alphabetic() {
309                    call.name.end = next_ix;
310                } else {
311                    return None;
312                }
313            }
314            // Commands start with a slash.
315            else if c == '/' {
316                call = Some(SlashCommandLine {
317                    name: next_ix..next_ix,
318                    arguments: Vec::new(),
319                });
320            }
321            // The line can't contain anything before the slash except for whitespace.
322            else if !c.is_whitespace() {
323                return None;
324            }
325            ix = next_ix;
326        }
327        call
328    }
329}
330
331pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
332    let mut label = CodeLabelBuilder::default();
333    label.push_str(command_name, None);
334    label.respan_filter_range(None);
335    label.push_str(" ", None);
336    label.push_str(
337        &arguments.join(" "),
338        cx.theme().syntax().highlight_id("comment").map(HighlightId),
339    );
340    label.build()
341}
342
343#[cfg(test)]
344mod tests {
345    use pretty_assertions::assert_eq;
346    use serde_json::json;
347
348    use super::*;
349
350    #[gpui::test]
351    async fn test_slash_command_output_to_events_round_trip() {
352        // Test basic output consisting of a single section.
353        {
354            let text = "Hello, world!".to_string();
355            let range = 0..text.len();
356            let output = SlashCommandOutput {
357                text,
358                sections: vec![SlashCommandOutputSection {
359                    range,
360                    icon: IconName::Code,
361                    label: "Section 1".into(),
362                    metadata: None,
363                }],
364                run_commands_in_text: false,
365            };
366
367            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
368            let events = events
369                .into_iter()
370                .filter_map(|event| event.ok())
371                .collect::<Vec<_>>();
372
373            assert_eq!(
374                events,
375                vec![
376                    SlashCommandEvent::StartSection {
377                        icon: IconName::Code,
378                        label: "Section 1".into(),
379                        metadata: None
380                    },
381                    SlashCommandEvent::Content(SlashCommandContent::Text {
382                        text: "Hello, world!".into(),
383                        run_commands_in_text: false
384                    }),
385                    SlashCommandEvent::EndSection
386                ]
387            );
388
389            let new_output =
390                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
391                    .await
392                    .unwrap();
393
394            assert_eq!(new_output, output);
395        }
396
397        // Test output where the sections do not comprise all of the text.
398        {
399            let text = "Apple\nCucumber\nBanana\n".to_string();
400            let output = SlashCommandOutput {
401                text,
402                sections: vec![
403                    SlashCommandOutputSection {
404                        range: 0..6,
405                        icon: IconName::Check,
406                        label: "Fruit".into(),
407                        metadata: None,
408                    },
409                    SlashCommandOutputSection {
410                        range: 15..22,
411                        icon: IconName::Check,
412                        label: "Fruit".into(),
413                        metadata: None,
414                    },
415                ],
416                run_commands_in_text: false,
417            };
418
419            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
420            let events = events
421                .into_iter()
422                .filter_map(|event| event.ok())
423                .collect::<Vec<_>>();
424
425            assert_eq!(
426                events,
427                vec![
428                    SlashCommandEvent::StartSection {
429                        icon: IconName::Check,
430                        label: "Fruit".into(),
431                        metadata: None
432                    },
433                    SlashCommandEvent::Content(SlashCommandContent::Text {
434                        text: "Apple\n".into(),
435                        run_commands_in_text: false
436                    }),
437                    SlashCommandEvent::EndSection,
438                    SlashCommandEvent::Content(SlashCommandContent::Text {
439                        text: "Cucumber\n".into(),
440                        run_commands_in_text: false
441                    }),
442                    SlashCommandEvent::StartSection {
443                        icon: IconName::Check,
444                        label: "Fruit".into(),
445                        metadata: None
446                    },
447                    SlashCommandEvent::Content(SlashCommandContent::Text {
448                        text: "Banana\n".into(),
449                        run_commands_in_text: false
450                    }),
451                    SlashCommandEvent::EndSection
452                ]
453            );
454
455            let new_output =
456                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
457                    .await
458                    .unwrap();
459
460            assert_eq!(new_output, output);
461        }
462
463        // Test output consisting of multiple sections.
464        {
465            let text = "Line 1\nLine 2\nLine 3\nLine 4\n".to_string();
466            let output = SlashCommandOutput {
467                text,
468                sections: vec![
469                    SlashCommandOutputSection {
470                        range: 0..6,
471                        icon: IconName::FileCode,
472                        label: "Section 1".into(),
473                        metadata: Some(json!({ "a": true })),
474                    },
475                    SlashCommandOutputSection {
476                        range: 7..13,
477                        icon: IconName::FileDoc,
478                        label: "Section 2".into(),
479                        metadata: Some(json!({ "b": true })),
480                    },
481                    SlashCommandOutputSection {
482                        range: 14..20,
483                        icon: IconName::FileGit,
484                        label: "Section 3".into(),
485                        metadata: Some(json!({ "c": true })),
486                    },
487                    SlashCommandOutputSection {
488                        range: 21..27,
489                        icon: IconName::FileToml,
490                        label: "Section 4".into(),
491                        metadata: Some(json!({ "d": true })),
492                    },
493                ],
494                run_commands_in_text: false,
495            };
496
497            let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
498            let events = events
499                .into_iter()
500                .filter_map(|event| event.ok())
501                .collect::<Vec<_>>();
502
503            assert_eq!(
504                events,
505                vec![
506                    SlashCommandEvent::StartSection {
507                        icon: IconName::FileCode,
508                        label: "Section 1".into(),
509                        metadata: Some(json!({ "a": true }))
510                    },
511                    SlashCommandEvent::Content(SlashCommandContent::Text {
512                        text: "Line 1".into(),
513                        run_commands_in_text: false
514                    }),
515                    SlashCommandEvent::EndSection,
516                    SlashCommandEvent::Content(SlashCommandContent::Text {
517                        text: "\n".into(),
518                        run_commands_in_text: false
519                    }),
520                    SlashCommandEvent::StartSection {
521                        icon: IconName::FileDoc,
522                        label: "Section 2".into(),
523                        metadata: Some(json!({ "b": true }))
524                    },
525                    SlashCommandEvent::Content(SlashCommandContent::Text {
526                        text: "Line 2".into(),
527                        run_commands_in_text: false
528                    }),
529                    SlashCommandEvent::EndSection,
530                    SlashCommandEvent::Content(SlashCommandContent::Text {
531                        text: "\n".into(),
532                        run_commands_in_text: false
533                    }),
534                    SlashCommandEvent::StartSection {
535                        icon: IconName::FileGit,
536                        label: "Section 3".into(),
537                        metadata: Some(json!({ "c": true }))
538                    },
539                    SlashCommandEvent::Content(SlashCommandContent::Text {
540                        text: "Line 3".into(),
541                        run_commands_in_text: false
542                    }),
543                    SlashCommandEvent::EndSection,
544                    SlashCommandEvent::Content(SlashCommandContent::Text {
545                        text: "\n".into(),
546                        run_commands_in_text: false
547                    }),
548                    SlashCommandEvent::StartSection {
549                        icon: IconName::FileToml,
550                        label: "Section 4".into(),
551                        metadata: Some(json!({ "d": true }))
552                    },
553                    SlashCommandEvent::Content(SlashCommandContent::Text {
554                        text: "Line 4".into(),
555                        run_commands_in_text: false
556                    }),
557                    SlashCommandEvent::EndSection,
558                    SlashCommandEvent::Content(SlashCommandContent::Text {
559                        text: "\n".into(),
560                        run_commands_in_text: false
561                    }),
562                ]
563            );
564
565            let new_output =
566                SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
567                    .await
568                    .unwrap();
569
570            assert_eq!(new_output, output);
571        }
572    }
573}