slash_command.rs

  1use crate::context_editor::ContextEditor;
  2use anyhow::Result;
  3pub use assistant_slash_command::SlashCommand;
  4use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
  5use editor::{CompletionProvider, Editor, ExcerptId};
  6use fuzzy::{StringMatchCandidate, match_strings};
  7use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
  8use language::{Anchor, Buffer, ToPoint};
  9use parking_lot::Mutex;
 10use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
 11use rope::Point;
 12use std::{
 13    cell::RefCell,
 14    ops::Range,
 15    rc::Rc,
 16    sync::{
 17        Arc,
 18        atomic::{AtomicBool, Ordering::SeqCst},
 19    },
 20};
 21use workspace::Workspace;
 22
 23pub struct SlashCommandCompletionProvider {
 24    cancel_flag: Mutex<Arc<AtomicBool>>,
 25    slash_commands: Arc<SlashCommandWorkingSet>,
 26    editor: Option<WeakEntity<ContextEditor>>,
 27    workspace: Option<WeakEntity<Workspace>>,
 28}
 29
 30impl SlashCommandCompletionProvider {
 31    pub fn new(
 32        slash_commands: Arc<SlashCommandWorkingSet>,
 33        editor: Option<WeakEntity<ContextEditor>>,
 34        workspace: Option<WeakEntity<Workspace>>,
 35    ) -> Self {
 36        Self {
 37            cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
 38            slash_commands,
 39            editor,
 40            workspace,
 41        }
 42    }
 43
 44    fn complete_command_name(
 45        &self,
 46        command_name: &str,
 47        command_range: Range<Anchor>,
 48        name_range: Range<Anchor>,
 49        window: &mut Window,
 50        cx: &mut App,
 51    ) -> Task<Result<Option<Vec<project::Completion>>>> {
 52        let slash_commands = self.slash_commands.clone();
 53        let candidates = slash_commands
 54            .command_names(cx)
 55            .into_iter()
 56            .enumerate()
 57            .map(|(ix, def)| StringMatchCandidate::new(ix, &def))
 58            .collect::<Vec<_>>();
 59        let command_name = command_name.to_string();
 60        let editor = self.editor.clone();
 61        let workspace = self.workspace.clone();
 62        window.spawn(cx, async move |cx| {
 63            let matches = match_strings(
 64                &candidates,
 65                &command_name,
 66                true,
 67                usize::MAX,
 68                &Default::default(),
 69                cx.background_executor().clone(),
 70            )
 71            .await;
 72
 73            cx.update(|_, cx| {
 74                Some(
 75                    matches
 76                        .into_iter()
 77                        .filter_map(|mat| {
 78                            let command = slash_commands.command(&mat.string, cx)?;
 79                            let mut new_text = mat.string.clone();
 80                            let requires_argument = command.requires_argument();
 81                            let accepts_arguments = command.accepts_arguments();
 82                            if requires_argument || accepts_arguments {
 83                                new_text.push(' ');
 84                            }
 85
 86                            let confirm =
 87                                editor
 88                                    .clone()
 89                                    .zip(workspace.clone())
 90                                    .map(|(editor, workspace)| {
 91                                        let command_name = mat.string.clone();
 92                                        let command_range = command_range.clone();
 93                                        let editor = editor.clone();
 94                                        let workspace = workspace.clone();
 95                                        Arc::new(
 96                                            move |intent: CompletionIntent,
 97                                            window: &mut Window,
 98                                            cx: &mut App| {
 99                                                if !requires_argument
100                                                && (!accepts_arguments || intent.is_complete())
101                                                {
102                                                    editor
103                                                        .update(cx, |editor, cx| {
104                                                            editor.run_command(
105                                                                command_range.clone(),
106                                                                &command_name,
107                                                                &[],
108                                                                true,
109                                                                workspace.clone(),
110                                                                window,
111                                                                cx,
112                                                            );
113                                                        })
114                                                        .ok();
115                                                    false
116                                                } else {
117                                                    requires_argument || accepts_arguments
118                                                }
119                                            },
120                                        ) as Arc<_>
121                                    });
122                            Some(project::Completion {
123                                replace_range: name_range.clone(),
124                                documentation: Some(CompletionDocumentation::SingleLine(
125                                    command.description().into(),
126                                )),
127                                new_text,
128                                label: command.label(cx),
129                                icon_path: None,
130                                insert_text_mode: None,
131                                confirm,
132                                source: CompletionSource::Custom,
133                            })
134                        })
135                        .collect(),
136                )
137            })
138        })
139    }
140
141    fn complete_command_argument(
142        &self,
143        command_name: &str,
144        arguments: &[String],
145        command_range: Range<Anchor>,
146        argument_range: Range<Anchor>,
147        last_argument_range: Range<Anchor>,
148        window: &mut Window,
149        cx: &mut App,
150    ) -> Task<Result<Option<Vec<project::Completion>>>> {
151        let new_cancel_flag = Arc::new(AtomicBool::new(false));
152        let mut flag = self.cancel_flag.lock();
153        flag.store(true, SeqCst);
154        *flag = new_cancel_flag.clone();
155        if let Some(command) = self.slash_commands.command(command_name, cx) {
156            let completions = command.complete_argument(
157                arguments,
158                new_cancel_flag.clone(),
159                self.workspace.clone(),
160                window,
161                cx,
162            );
163            let command_name: Arc<str> = command_name.into();
164            let editor = self.editor.clone();
165            let workspace = self.workspace.clone();
166            let arguments = arguments.to_vec();
167            cx.background_spawn(async move {
168                Ok(Some(
169                    completions
170                        .await?
171                        .into_iter()
172                        .map(|new_argument| {
173                            let confirm =
174                                editor
175                                    .clone()
176                                    .zip(workspace.clone())
177                                    .map(|(editor, workspace)| {
178                                        Arc::new({
179                                            let mut completed_arguments = arguments.clone();
180                                            if new_argument.replace_previous_arguments {
181                                                completed_arguments.clear();
182                                            } else {
183                                                completed_arguments.pop();
184                                            }
185                                            completed_arguments.push(new_argument.new_text.clone());
186
187                                            let command_range = command_range.clone();
188                                            let command_name = command_name.clone();
189                                            move |intent: CompletionIntent,
190                                              window: &mut Window,
191                                              cx: &mut App| {
192                                            if new_argument.after_completion.run()
193                                                || intent.is_complete()
194                                            {
195                                                editor
196                                                    .update(cx, |editor, cx| {
197                                                        editor.run_command(
198                                                            command_range.clone(),
199                                                            &command_name,
200                                                            &completed_arguments,
201                                                            true,
202                                                            workspace.clone(),
203                                                            window,
204                                                            cx,
205                                                        );
206                                                    })
207                                                    .ok();
208                                                false
209                                            } else {
210                                                !new_argument.after_completion.run()
211                                            }
212                                        }
213                                        }) as Arc<_>
214                                    });
215
216                            let mut new_text = new_argument.new_text.clone();
217                            if new_argument.after_completion == AfterCompletion::Continue {
218                                new_text.push(' ');
219                            }
220
221                            project::Completion {
222                                replace_range: if new_argument.replace_previous_arguments {
223                                    argument_range.clone()
224                                } else {
225                                    last_argument_range.clone()
226                                },
227                                label: new_argument.label,
228                                icon_path: None,
229                                new_text,
230                                documentation: None,
231                                confirm,
232                                insert_text_mode: None,
233                                source: CompletionSource::Custom,
234                            }
235                        })
236                        .collect(),
237                ))
238            })
239        } else {
240            Task::ready(Ok(Some(Vec::new())))
241        }
242    }
243}
244
245impl CompletionProvider for SlashCommandCompletionProvider {
246    fn completions(
247        &self,
248        _excerpt_id: ExcerptId,
249        buffer: &Entity<Buffer>,
250        buffer_position: Anchor,
251        _: editor::CompletionContext,
252        window: &mut Window,
253        cx: &mut Context<Editor>,
254    ) -> Task<Result<Option<Vec<project::Completion>>>> {
255        let Some((name, arguments, command_range, last_argument_range)) =
256            buffer.update(cx, |buffer, _cx| {
257                let position = buffer_position.to_point(buffer);
258                let line_start = Point::new(position.row, 0);
259                let mut lines = buffer.text_for_range(line_start..position).lines();
260                let line = lines.next()?;
261                let call = SlashCommandLine::parse(line)?;
262
263                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
264                let command_range_end = Point::new(
265                    position.row,
266                    call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
267                );
268                let command_range = buffer.anchor_after(command_range_start)
269                    ..buffer.anchor_after(command_range_end);
270
271                let name = line[call.name.clone()].to_string();
272                let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
273                {
274                    let last_arg_start =
275                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
276                    let first_arg_start = call.arguments.first().expect("we have the last element");
277                    let first_arg_start =
278                        buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
279                    let arguments = call
280                        .arguments
281                        .into_iter()
282                        .filter_map(|argument| Some(line.get(argument)?.to_string()))
283                        .collect::<Vec<_>>();
284                    let argument_range = first_arg_start..buffer_position;
285                    (
286                        Some((arguments, argument_range)),
287                        last_arg_start..buffer_position,
288                    )
289                } else {
290                    let start =
291                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
292                    (None, start..buffer_position)
293                };
294
295                Some((name, arguments, command_range, last_argument_range))
296            })
297        else {
298            return Task::ready(Ok(Some(Vec::new())));
299        };
300
301        if let Some((arguments, argument_range)) = arguments {
302            self.complete_command_argument(
303                &name,
304                &arguments,
305                command_range,
306                argument_range,
307                last_argument_range,
308                window,
309                cx,
310            )
311        } else {
312            self.complete_command_name(&name, command_range, last_argument_range, window, cx)
313        }
314    }
315
316    fn resolve_completions(
317        &self,
318        _: Entity<Buffer>,
319        _: Vec<usize>,
320        _: Rc<RefCell<Box<[project::Completion]>>>,
321        _: &mut Context<Editor>,
322    ) -> Task<Result<bool>> {
323        Task::ready(Ok(true))
324    }
325
326    fn is_completion_trigger(
327        &self,
328        buffer: &Entity<Buffer>,
329        position: language::Anchor,
330        _text: &str,
331        _trigger_in_words: bool,
332        cx: &mut Context<Editor>,
333    ) -> bool {
334        let buffer = buffer.read(cx);
335        let position = position.to_point(buffer);
336        let line_start = Point::new(position.row, 0);
337        let mut lines = buffer.text_for_range(line_start..position).lines();
338        if let Some(line) = lines.next() {
339            SlashCommandLine::parse(line).is_some()
340        } else {
341            false
342        }
343    }
344
345    fn sort_completions(&self) -> bool {
346        false
347    }
348}