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