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