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