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        cx: &mut ViewContext<Editor>,
221    ) -> Task<Result<Vec<project::Completion>>> {
222        let Some((name, argument, command_range, argument_range)) =
223            buffer.update(cx, |buffer, _cx| {
224                let position = buffer_position.to_point(buffer);
225                let line_start = Point::new(position.row, 0);
226                let mut lines = buffer.text_for_range(line_start..position).lines();
227                let line = lines.next()?;
228                let call = SlashCommandLine::parse(line)?;
229
230                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
231                let command_range_end = Point::new(
232                    position.row,
233                    call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
234                );
235                let command_range = buffer.anchor_after(command_range_start)
236                    ..buffer.anchor_after(command_range_end);
237
238                let name = line[call.name.clone()].to_string();
239
240                Some(if let Some(argument) = call.argument {
241                    let start =
242                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
243                    let argument = line[argument.clone()].to_string();
244                    (name, Some(argument), command_range, start..buffer_position)
245                } else {
246                    let start =
247                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
248                    (name, None, command_range, start..buffer_position)
249                })
250            })
251        else {
252            return Task::ready(Ok(Vec::new()));
253        };
254
255        if let Some(argument) = argument {
256            self.complete_command_argument(&name, argument, command_range, argument_range, cx)
257        } else {
258            self.complete_command_name(&name, command_range, argument_range, cx)
259        }
260    }
261
262    fn resolve_completions(
263        &self,
264        _: Model<Buffer>,
265        _: Vec<usize>,
266        _: Arc<RwLock<Box<[project::Completion]>>>,
267        _: &mut ViewContext<Editor>,
268    ) -> Task<Result<bool>> {
269        Task::ready(Ok(true))
270    }
271
272    fn apply_additional_edits_for_completion(
273        &self,
274        _: Model<Buffer>,
275        _: project::Completion,
276        _: bool,
277        _: &mut ViewContext<Editor>,
278    ) -> Task<Result<Option<language::Transaction>>> {
279        Task::ready(Ok(None))
280    }
281
282    fn is_completion_trigger(
283        &self,
284        buffer: &Model<Buffer>,
285        position: language::Anchor,
286        _text: &str,
287        _trigger_in_words: bool,
288        cx: &mut ViewContext<Editor>,
289    ) -> bool {
290        let buffer = buffer.read(cx);
291        let position = position.to_point(buffer);
292        let line_start = Point::new(position.row, 0);
293        let mut lines = buffer.text_for_range(line_start..position).lines();
294        if let Some(line) = lines.next() {
295            SlashCommandLine::parse(line).is_some()
296        } else {
297            false
298        }
299    }
300}
301
302impl SlashCommandLine {
303    pub(crate) fn parse(line: &str) -> Option<Self> {
304        let mut call: Option<Self> = None;
305        let mut ix = 0;
306        for c in line.chars() {
307            let next_ix = ix + c.len_utf8();
308            if let Some(call) = &mut call {
309                // The command arguments start at the first non-whitespace character
310                // after the command name, and continue until the end of the line.
311                if let Some(argument) = &mut call.argument {
312                    if (*argument).is_empty() && c.is_whitespace() {
313                        argument.start = next_ix;
314                    }
315                    argument.end = next_ix;
316                }
317                // The command name ends at the first whitespace character.
318                else if !call.name.is_empty() {
319                    if c.is_whitespace() {
320                        call.argument = Some(next_ix..next_ix);
321                    } else {
322                        call.name.end = next_ix;
323                    }
324                }
325                // The command name must begin with a letter.
326                else if c.is_alphabetic() {
327                    call.name.end = next_ix;
328                } else {
329                    return None;
330                }
331            }
332            // Commands start with a slash.
333            else if c == '/' {
334                call = Some(SlashCommandLine {
335                    name: next_ix..next_ix,
336                    argument: None,
337                });
338            }
339            // The line can't contain anything before the slash except for whitespace.
340            else if !c.is_whitespace() {
341                return None;
342            }
343            ix = next_ix;
344        }
345        call
346    }
347}