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