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