slash_command.rs

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