slash_command.rs

  1use crate::assistant_panel::ContextEditor;
  2use anyhow::Result;
  3pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
  4use editor::{CompletionProvider, Editor};
  5use fuzzy::{match_strings, StringMatchCandidate};
  6use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
  7use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
  8use parking_lot::{Mutex, RwLock};
  9use project::CompletionIntent;
 10use rope::Point;
 11use std::{
 12    ops::Range,
 13    sync::{
 14        atomic::{AtomicBool, Ordering::SeqCst},
 15        Arc,
 16    },
 17};
 18use ui::ActiveTheme;
 19use workspace::Workspace;
 20
 21pub mod default_command;
 22pub mod diagnostics_command;
 23pub mod docs_command;
 24pub mod fetch_command;
 25pub mod file_command;
 26pub mod now_command;
 27pub mod project_command;
 28pub mod prompt_command;
 29pub mod search_command;
 30pub mod symbols_command;
 31pub mod tabs_command;
 32pub mod terminal_command;
 33pub mod workflow_command;
 34
 35pub(crate) struct SlashCommandCompletionProvider {
 36    cancel_flag: Mutex<Arc<AtomicBool>>,
 37    editor: Option<WeakView<ContextEditor>>,
 38    workspace: Option<WeakView<Workspace>>,
 39}
 40
 41pub(crate) struct SlashCommandLine {
 42    /// The range within the line containing the command name.
 43    pub name: Range<usize>,
 44    /// The range within the line containing the command argument.
 45    pub argument: Option<Range<usize>>,
 46}
 47
 48impl SlashCommandCompletionProvider {
 49    pub fn new(
 50        editor: Option<WeakView<ContextEditor>>,
 51        workspace: Option<WeakView<Workspace>>,
 52    ) -> Self {
 53        Self {
 54            cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
 55            editor,
 56            workspace,
 57        }
 58    }
 59
 60    fn complete_command_name(
 61        &self,
 62        command_name: &str,
 63        command_range: Range<Anchor>,
 64        name_range: Range<Anchor>,
 65        cx: &mut WindowContext,
 66    ) -> Task<Result<Vec<project::Completion>>> {
 67        let commands = SlashCommandRegistry::global(cx);
 68        let candidates = commands
 69            .command_names()
 70            .into_iter()
 71            .enumerate()
 72            .map(|(ix, def)| StringMatchCandidate {
 73                id: ix,
 74                string: def.to_string(),
 75                char_bag: def.as_ref().into(),
 76            })
 77            .collect::<Vec<_>>();
 78        let command_name = command_name.to_string();
 79        let editor = self.editor.clone();
 80        let workspace = self.workspace.clone();
 81        cx.spawn(|mut cx| async move {
 82            let matches = match_strings(
 83                &candidates,
 84                &command_name,
 85                true,
 86                usize::MAX,
 87                &Default::default(),
 88                cx.background_executor().clone(),
 89            )
 90            .await;
 91
 92            cx.update(|cx| {
 93                matches
 94                    .into_iter()
 95                    .filter_map(|mat| {
 96                        let command = commands.command(&mat.string)?;
 97                        let mut new_text = mat.string.clone();
 98                        let requires_argument = command.requires_argument();
 99                        if requires_argument {
100                            new_text.push(' ');
101                        }
102
103                        let confirm = editor.clone().zip(workspace.clone()).and_then(
104                            |(editor, workspace)| {
105                                (!requires_argument).then(|| {
106                                    let command_name = mat.string.clone();
107                                    let command_range = command_range.clone();
108                                    let editor = editor.clone();
109                                    let workspace = workspace.clone();
110                                    Arc::new(
111                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
112                                            if intent.is_complete() {
113                                                editor
114                                                    .update(cx, |editor, cx| {
115                                                        editor.run_command(
116                                                            command_range.clone(),
117                                                            &command_name,
118                                                            None,
119                                                            true,
120                                                            workspace.clone(),
121                                                            cx,
122                                                        );
123                                                    })
124                                                    .ok();
125                                            }
126                                        },
127                                    ) as Arc<_>
128                                })
129                            },
130                        );
131                        Some(project::Completion {
132                            old_range: name_range.clone(),
133                            documentation: Some(Documentation::SingleLine(command.description())),
134                            new_text,
135                            label: command.label(cx),
136                            server_id: LanguageServerId(0),
137                            lsp_completion: Default::default(),
138                            show_new_completions_on_confirm: requires_argument,
139                            confirm,
140                        })
141                    })
142                    .collect()
143            })
144        })
145    }
146
147    fn complete_command_argument(
148        &self,
149        command_name: &str,
150        argument: String,
151        command_range: Range<Anchor>,
152        argument_range: Range<Anchor>,
153        cx: &mut WindowContext,
154    ) -> Task<Result<Vec<project::Completion>>> {
155        let new_cancel_flag = Arc::new(AtomicBool::new(false));
156        let mut flag = self.cancel_flag.lock();
157        flag.store(true, SeqCst);
158        *flag = new_cancel_flag.clone();
159        let commands = SlashCommandRegistry::global(cx);
160        if let Some(command) = commands.command(command_name) {
161            let completions = command.complete_argument(
162                argument,
163                new_cancel_flag.clone(),
164                self.workspace.clone(),
165                cx,
166            );
167            let command_name: Arc<str> = command_name.into();
168            let editor = self.editor.clone();
169            let workspace = self.workspace.clone();
170            cx.background_executor().spawn(async move {
171                Ok(completions
172                    .await?
173                    .into_iter()
174                    .map(|command_argument| {
175                        let confirm = if command_argument.run_command {
176                            editor
177                                .clone()
178                                .zip(workspace.clone())
179                                .map(|(editor, workspace)| {
180                                    Arc::new({
181                                        let command_range = command_range.clone();
182                                        let command_name = command_name.clone();
183                                        let command_argument = command_argument.new_text.clone();
184                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
185                                            if intent.is_complete() {
186                                                editor
187                                                    .update(cx, |editor, cx| {
188                                                        editor.run_command(
189                                                            command_range.clone(),
190                                                            &command_name,
191                                                            Some(&command_argument),
192                                                            true,
193                                                            workspace.clone(),
194                                                            cx,
195                                                        );
196                                                    })
197                                                    .ok();
198                                            }
199                                        }
200                                    }) as Arc<_>
201                                })
202                        } else {
203                            None
204                        };
205
206                        let mut new_text = command_argument.new_text.clone();
207                        if !command_argument.run_command {
208                            new_text.push(' ');
209                        }
210
211                        project::Completion {
212                            old_range: argument_range.clone(),
213                            label: command_argument.label,
214                            new_text,
215                            documentation: None,
216                            server_id: LanguageServerId(0),
217                            lsp_completion: Default::default(),
218                            show_new_completions_on_confirm: !command_argument.run_command,
219                            confirm,
220                        }
221                    })
222                    .collect())
223            })
224        } else {
225            cx.background_executor()
226                .spawn(async move { Ok(Vec::new()) })
227        }
228    }
229}
230
231impl CompletionProvider for SlashCommandCompletionProvider {
232    fn completions(
233        &self,
234        buffer: &Model<Buffer>,
235        buffer_position: Anchor,
236        _: editor::CompletionContext,
237        cx: &mut ViewContext<Editor>,
238    ) -> Task<Result<Vec<project::Completion>>> {
239        let Some((name, argument, command_range, argument_range)) =
240            buffer.update(cx, |buffer, _cx| {
241                let position = buffer_position.to_point(buffer);
242                let line_start = Point::new(position.row, 0);
243                let mut lines = buffer.text_for_range(line_start..position).lines();
244                let line = lines.next()?;
245                let call = SlashCommandLine::parse(line)?;
246
247                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
248                let command_range_end = Point::new(
249                    position.row,
250                    call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
251                );
252                let command_range = buffer.anchor_after(command_range_start)
253                    ..buffer.anchor_after(command_range_end);
254
255                let name = line[call.name.clone()].to_string();
256
257                Some(if let Some(argument) = call.argument {
258                    let start =
259                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
260                    let argument = line[argument.clone()].to_string();
261                    (name, Some(argument), command_range, start..buffer_position)
262                } else {
263                    let start =
264                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
265                    (name, None, command_range, start..buffer_position)
266                })
267            })
268        else {
269            return Task::ready(Ok(Vec::new()));
270        };
271
272        if let Some(argument) = argument {
273            self.complete_command_argument(&name, argument, command_range, argument_range, cx)
274        } else {
275            self.complete_command_name(&name, command_range, argument_range, cx)
276        }
277    }
278
279    fn resolve_completions(
280        &self,
281        _: Model<Buffer>,
282        _: Vec<usize>,
283        _: Arc<RwLock<Box<[project::Completion]>>>,
284        _: &mut ViewContext<Editor>,
285    ) -> Task<Result<bool>> {
286        Task::ready(Ok(true))
287    }
288
289    fn apply_additional_edits_for_completion(
290        &self,
291        _: Model<Buffer>,
292        _: project::Completion,
293        _: bool,
294        _: &mut ViewContext<Editor>,
295    ) -> Task<Result<Option<language::Transaction>>> {
296        Task::ready(Ok(None))
297    }
298
299    fn is_completion_trigger(
300        &self,
301        buffer: &Model<Buffer>,
302        position: language::Anchor,
303        _text: &str,
304        _trigger_in_words: bool,
305        cx: &mut ViewContext<Editor>,
306    ) -> bool {
307        let buffer = buffer.read(cx);
308        let position = position.to_point(buffer);
309        let line_start = Point::new(position.row, 0);
310        let mut lines = buffer.text_for_range(line_start..position).lines();
311        if let Some(line) = lines.next() {
312            SlashCommandLine::parse(line).is_some()
313        } else {
314            false
315        }
316    }
317}
318
319impl SlashCommandLine {
320    pub(crate) fn parse(line: &str) -> Option<Self> {
321        let mut call: Option<Self> = None;
322        let mut ix = 0;
323        for c in line.chars() {
324            let next_ix = ix + c.len_utf8();
325            if let Some(call) = &mut call {
326                // The command arguments start at the first non-whitespace character
327                // after the command name, and continue until the end of the line.
328                if let Some(argument) = &mut call.argument {
329                    if (*argument).is_empty() && c.is_whitespace() {
330                        argument.start = next_ix;
331                    }
332                    argument.end = next_ix;
333                }
334                // The command name ends at the first whitespace character.
335                else if !call.name.is_empty() {
336                    if c.is_whitespace() {
337                        call.argument = Some(next_ix..next_ix);
338                    } else {
339                        call.name.end = next_ix;
340                    }
341                }
342                // The command name must begin with a letter.
343                else if c.is_alphabetic() {
344                    call.name.end = next_ix;
345                } else {
346                    return None;
347                }
348            }
349            // Commands start with a slash.
350            else if c == '/' {
351                call = Some(SlashCommandLine {
352                    name: next_ix..next_ix,
353                    argument: None,
354                });
355            }
356            // The line can't contain anything before the slash except for whitespace.
357            else if !c.is_whitespace() {
358                return None;
359            }
360            ix = next_ix;
361        }
362        call
363    }
364}
365
366pub fn create_label_for_command(
367    command_name: &str,
368    arguments: &[&str],
369    cx: &AppContext,
370) -> CodeLabel {
371    let mut label = CodeLabel::default();
372    label.push_str(command_name, None);
373    label.push_str(" ", None);
374    label.push_str(
375        &arguments.join(" "),
376        cx.theme().syntax().highlight_id("comment").map(HighlightId),
377    );
378    label.filter_range = 0..command_name.len();
379    label
380}