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