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<Vec<project::CompletionResponse>>> {
 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                let completions = 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
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                vec![project::CompletionResponse {
138                    completions,
139                    is_incomplete: false,
140                }]
141            })
142        })
143    }
144
145    fn complete_command_argument(
146        &self,
147        command_name: &str,
148        arguments: &[String],
149        command_range: Range<Anchor>,
150        argument_range: Range<Anchor>,
151        last_argument_range: Range<Anchor>,
152        window: &mut Window,
153        cx: &mut App,
154    ) -> Task<Result<Vec<project::CompletionResponse>>> {
155        let new_cancel_flag = Arc::new(AtomicBool::new(false));
156        let mut flag = self.cancel_flag.lock();
157        flag.store(true, SeqCst);
158        *flag = new_cancel_flag.clone();
159        if let Some(command) = self.slash_commands.command(command_name, cx) {
160            let completions = command.complete_argument(
161                arguments,
162                new_cancel_flag.clone(),
163                self.workspace.clone(),
164                window,
165                cx,
166            );
167            let command_name: Arc<str> = command_name.into();
168            let editor = self.editor.clone();
169            let workspace = self.workspace.clone();
170            let arguments = arguments.to_vec();
171            cx.background_spawn(async move {
172                let completions = completions
173                    .await?
174                    .into_iter()
175                    .map(|new_argument| {
176                        let confirm =
177                            editor
178                                .clone()
179                                .zip(workspace.clone())
180                                .map(|(editor, workspace)| {
181                                    Arc::new({
182                                        let mut completed_arguments = arguments.clone();
183                                        if new_argument.replace_previous_arguments {
184                                            completed_arguments.clear();
185                                        } else {
186                                            completed_arguments.pop();
187                                        }
188                                        completed_arguments.push(new_argument.new_text.clone());
189
190                                        let command_range = command_range.clone();
191                                        let command_name = command_name.clone();
192                                        move |intent: CompletionIntent,
193                                              window: &mut Window,
194                                              cx: &mut App| {
195                                            if new_argument.after_completion.run()
196                                                || intent.is_complete()
197                                            {
198                                                editor
199                                                    .update(cx, |editor, cx| {
200                                                        editor.run_command(
201                                                            command_range.clone(),
202                                                            &command_name,
203                                                            &completed_arguments,
204                                                            true,
205                                                            workspace.clone(),
206                                                            window,
207                                                            cx,
208                                                        );
209                                                    })
210                                                    .ok();
211                                                false
212                                            } else {
213                                                !new_argument.after_completion.run()
214                                            }
215                                        }
216                                    }) as Arc<_>
217                                });
218
219                        let mut new_text = new_argument.new_text.clone();
220                        if new_argument.after_completion == AfterCompletion::Continue {
221                            new_text.push(' ');
222                        }
223
224                        project::Completion {
225                            replace_range: if new_argument.replace_previous_arguments {
226                                argument_range.clone()
227                            } else {
228                                last_argument_range.clone()
229                            },
230                            label: new_argument.label,
231                            icon_path: None,
232                            new_text,
233                            documentation: None,
234                            confirm,
235                            insert_text_mode: None,
236                            source: CompletionSource::Custom,
237                        }
238                    })
239                    .collect();
240
241                Ok(vec![project::CompletionResponse {
242                    completions,
243                    is_incomplete: false,
244                }])
245            })
246        } else {
247            Task::ready(Ok(vec![project::CompletionResponse {
248                completions: Vec::new(),
249                is_incomplete: false,
250            }]))
251        }
252    }
253}
254
255impl CompletionProvider for SlashCommandCompletionProvider {
256    fn completions(
257        &self,
258        _excerpt_id: ExcerptId,
259        buffer: &Entity<Buffer>,
260        buffer_position: Anchor,
261        _: editor::CompletionContext,
262        window: &mut Window,
263        cx: &mut Context<Editor>,
264    ) -> Task<Result<Vec<project::CompletionResponse>>> {
265        let Some((name, arguments, command_range, last_argument_range)) =
266            buffer.update(cx, |buffer, _cx| {
267                let position = buffer_position.to_point(buffer);
268                let line_start = Point::new(position.row, 0);
269                let mut lines = buffer.text_for_range(line_start..position).lines();
270                let line = lines.next()?;
271                let call = SlashCommandLine::parse(line)?;
272
273                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
274                let command_range_end = Point::new(
275                    position.row,
276                    call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
277                );
278                let command_range = buffer.anchor_after(command_range_start)
279                    ..buffer.anchor_after(command_range_end);
280
281                let name = line[call.name.clone()].to_string();
282                let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
283                {
284                    let last_arg_start =
285                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
286                    let first_arg_start = call.arguments.first().expect("we have the last element");
287                    let first_arg_start =
288                        buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
289                    let arguments = call
290                        .arguments
291                        .into_iter()
292                        .filter_map(|argument| Some(line.get(argument)?.to_string()))
293                        .collect::<Vec<_>>();
294                    let argument_range = first_arg_start..buffer_position;
295                    (
296                        Some((arguments, argument_range)),
297                        last_arg_start..buffer_position,
298                    )
299                } else {
300                    let start =
301                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
302                    (None, start..buffer_position)
303                };
304
305                Some((name, arguments, command_range, last_argument_range))
306            })
307        else {
308            return Task::ready(Ok(vec![project::CompletionResponse {
309                completions: Vec::new(),
310                is_incomplete: false,
311            }]));
312        };
313
314        if let Some((arguments, argument_range)) = arguments {
315            self.complete_command_argument(
316                &name,
317                &arguments,
318                command_range,
319                argument_range,
320                last_argument_range,
321                window,
322                cx,
323            )
324        } else {
325            self.complete_command_name(&name, command_range, last_argument_range, window, cx)
326        }
327    }
328
329    fn resolve_completions(
330        &self,
331        _: Entity<Buffer>,
332        _: Vec<usize>,
333        _: Rc<RefCell<Box<[project::Completion]>>>,
334        _: &mut Context<Editor>,
335    ) -> Task<Result<bool>> {
336        Task::ready(Ok(true))
337    }
338
339    fn is_completion_trigger(
340        &self,
341        buffer: &Entity<Buffer>,
342        position: language::Anchor,
343        _text: &str,
344        _trigger_in_words: bool,
345        cx: &mut Context<Editor>,
346    ) -> bool {
347        let buffer = buffer.read(cx);
348        let position = position.to_point(buffer);
349        let line_start = Point::new(position.row, 0);
350        let mut lines = buffer.text_for_range(line_start..position).lines();
351        if let Some(line) = lines.next() {
352            SlashCommandLine::parse(line).is_some()
353        } else {
354            false
355        }
356    }
357
358    fn sort_completions(&self) -> bool {
359        false
360    }
361}