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::{Model, Task, ViewContext, WeakView, WindowContext};
  7use language::{Anchor, Buffer, CodeLabel, Documentation, 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 workspace::Workspace;
 18
 19pub mod active_command;
 20pub mod default_command;
 21pub mod fetch_command;
 22pub mod file_command;
 23pub mod now_command;
 24pub mod project_command;
 25pub mod prompt_command;
 26pub mod rustdoc_command;
 27pub mod search_command;
 28pub mod tabs_command;
 29
 30pub(crate) struct SlashCommandCompletionProvider {
 31    commands: Arc<SlashCommandRegistry>,
 32    cancel_flag: Mutex<Arc<AtomicBool>>,
 33    editor: Option<WeakView<ContextEditor>>,
 34    workspace: Option<WeakView<Workspace>>,
 35}
 36
 37pub(crate) struct SlashCommandLine {
 38    /// The range within the line containing the command name.
 39    pub name: Range<usize>,
 40    /// The range within the line containing the command argument.
 41    pub argument: Option<Range<usize>>,
 42}
 43
 44impl SlashCommandCompletionProvider {
 45    pub fn new(
 46        commands: Arc<SlashCommandRegistry>,
 47        editor: Option<WeakView<ContextEditor>>,
 48        workspace: Option<WeakView<Workspace>>,
 49    ) -> Self {
 50        Self {
 51            cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
 52            editor,
 53            commands,
 54            workspace,
 55        }
 56    }
 57
 58    fn complete_command_name(
 59        &self,
 60        command_name: &str,
 61        command_range: Range<Anchor>,
 62        name_range: Range<Anchor>,
 63        cx: &mut WindowContext,
 64    ) -> Task<Result<Vec<project::Completion>>> {
 65        let candidates = self
 66            .commands
 67            .command_names()
 68            .into_iter()
 69            .enumerate()
 70            .map(|(ix, def)| StringMatchCandidate {
 71                id: ix,
 72                string: def.to_string(),
 73                char_bag: def.as_ref().into(),
 74            })
 75            .collect::<Vec<_>>();
 76        let commands = self.commands.clone();
 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        if let Some(command) = self.commands.command(command_name) {
156            let completions = command.complete_argument(
157                argument,
158                new_cancel_flag.clone(),
159                self.workspace.clone(),
160                cx,
161            );
162            let command_name: Arc<str> = command_name.into();
163            let editor = self.editor.clone();
164            let workspace = self.workspace.clone();
165            cx.background_executor().spawn(async move {
166                Ok(completions
167                    .await?
168                    .into_iter()
169                    .map(|command_argument| {
170                        let confirm =
171                            editor
172                                .clone()
173                                .zip(workspace.clone())
174                                .map(|(editor, workspace)| {
175                                    Arc::new({
176                                        let command_range = command_range.clone();
177                                        let command_name = command_name.clone();
178                                        let command_argument = command_argument.clone();
179                                        move |cx: &mut WindowContext| {
180                                            editor
181                                                .update(cx, |editor, cx| {
182                                                    editor.run_command(
183                                                        command_range.clone(),
184                                                        &command_name,
185                                                        Some(&command_argument),
186                                                        true,
187                                                        workspace.clone(),
188                                                        cx,
189                                                    );
190                                                })
191                                                .ok();
192                                        }
193                                    }) as Arc<_>
194                                });
195                        project::Completion {
196                            old_range: argument_range.clone(),
197                            label: CodeLabel::plain(command_argument.clone(), None),
198                            new_text: command_argument.clone(),
199                            documentation: None,
200                            server_id: LanguageServerId(0),
201                            lsp_completion: Default::default(),
202                            show_new_completions_on_confirm: false,
203                            confirm,
204                        }
205                    })
206                    .collect())
207            })
208        } else {
209            cx.background_executor()
210                .spawn(async move { Ok(Vec::new()) })
211        }
212    }
213}
214
215impl CompletionProvider for SlashCommandCompletionProvider {
216    fn completions(
217        &self,
218        buffer: &Model<Buffer>,
219        buffer_position: Anchor,
220        _: editor::CompletionContext,
221        cx: &mut ViewContext<Editor>,
222    ) -> Task<Result<Vec<project::Completion>>> {
223        let Some((name, argument, command_range, argument_range)) =
224            buffer.update(cx, |buffer, _cx| {
225                let position = buffer_position.to_point(buffer);
226                let line_start = Point::new(position.row, 0);
227                let mut lines = buffer.text_for_range(line_start..position).lines();
228                let line = lines.next()?;
229                let call = SlashCommandLine::parse(line)?;
230
231                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
232                let command_range_end = Point::new(
233                    position.row,
234                    call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
235                );
236                let command_range = buffer.anchor_after(command_range_start)
237                    ..buffer.anchor_after(command_range_end);
238
239                let name = line[call.name.clone()].to_string();
240
241                Some(if let Some(argument) = call.argument {
242                    let start =
243                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
244                    let argument = line[argument.clone()].to_string();
245                    (name, Some(argument), command_range, start..buffer_position)
246                } else {
247                    let start =
248                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
249                    (name, None, command_range, start..buffer_position)
250                })
251            })
252        else {
253            return Task::ready(Ok(Vec::new()));
254        };
255
256        if let Some(argument) = argument {
257            self.complete_command_argument(&name, argument, command_range, argument_range, cx)
258        } else {
259            self.complete_command_name(&name, command_range, argument_range, cx)
260        }
261    }
262
263    fn resolve_completions(
264        &self,
265        _: Model<Buffer>,
266        _: Vec<usize>,
267        _: Arc<RwLock<Box<[project::Completion]>>>,
268        _: &mut ViewContext<Editor>,
269    ) -> Task<Result<bool>> {
270        Task::ready(Ok(true))
271    }
272
273    fn apply_additional_edits_for_completion(
274        &self,
275        _: Model<Buffer>,
276        _: project::Completion,
277        _: bool,
278        _: &mut ViewContext<Editor>,
279    ) -> Task<Result<Option<language::Transaction>>> {
280        Task::ready(Ok(None))
281    }
282
283    fn is_completion_trigger(
284        &self,
285        buffer: &Model<Buffer>,
286        position: language::Anchor,
287        _text: &str,
288        _trigger_in_words: bool,
289        cx: &mut ViewContext<Editor>,
290    ) -> bool {
291        let buffer = buffer.read(cx);
292        let position = position.to_point(buffer);
293        let line_start = Point::new(position.row, 0);
294        let mut lines = buffer.text_for_range(line_start..position).lines();
295        if let Some(line) = lines.next() {
296            SlashCommandLine::parse(line).is_some()
297        } else {
298            false
299        }
300    }
301}
302
303impl SlashCommandLine {
304    pub(crate) fn parse(line: &str) -> Option<Self> {
305        let mut call: Option<Self> = None;
306        let mut ix = 0;
307        for c in line.chars() {
308            let next_ix = ix + c.len_utf8();
309            if let Some(call) = &mut call {
310                // The command arguments start at the first non-whitespace character
311                // after the command name, and continue until the end of the line.
312                if let Some(argument) = &mut call.argument {
313                    if (*argument).is_empty() && c.is_whitespace() {
314                        argument.start = next_ix;
315                    }
316                    argument.end = next_ix;
317                }
318                // The command name ends at the first whitespace character.
319                else if !call.name.is_empty() {
320                    if c.is_whitespace() {
321                        call.argument = Some(next_ix..next_ix);
322                    } else {
323                        call.name.end = next_ix;
324                    }
325                }
326                // The command name must begin with a letter.
327                else if c.is_alphabetic() {
328                    call.name.end = next_ix;
329                } else {
330                    return None;
331                }
332            }
333            // Commands start with a slash.
334            else if c == '/' {
335                call = Some(SlashCommandLine {
336                    name: next_ix..next_ix,
337                    argument: None,
338                });
339            }
340            // The line can't contain anything before the slash except for whitespace.
341            else if !c.is_whitespace() {
342                return None;
343            }
344            ix = next_ix;
345        }
346        call
347    }
348}