slash_command.rs

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