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