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::{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<TextThreadEditor>>,
 25    workspace: Option<WeakEntity<Workspace>>,
 26}
 27
 28impl SlashCommandCompletionProvider {
 29    pub fn new(
 30        slash_commands: Arc<SlashCommandWorkingSet>,
 31        editor: Option<WeakEntity<TextThreadEditor>>,
 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                true,
 66                usize::MAX,
 67                &Default::default(),
 68                cx.background_executor().clone(),
 69            )
 70            .await;
 71
 72            cx.update(|_, cx| {
 73                let completions = matches
 74                    .into_iter()
 75                    .filter_map(|mat| {
 76                        let command = slash_commands.command(&mat.string, cx)?;
 77                        let mut new_text = mat.string.clone();
 78                        let requires_argument = command.requires_argument();
 79                        let accepts_arguments = command.accepts_arguments();
 80                        if requires_argument || accepts_arguments {
 81                            new_text.push(' ');
 82                        }
 83
 84                        let confirm =
 85                            editor
 86                                .clone()
 87                                .zip(workspace.clone())
 88                                .map(|(editor, workspace)| {
 89                                    let command_name = mat.string.clone();
 90                                    let command_range = command_range.clone();
 91                                    Arc::new(
 92                                            move |intent: CompletionIntent,
 93                                            window: &mut Window,
 94                                            cx: &mut App| {
 95                                                if !requires_argument
 96                                                && (!accepts_arguments || intent.is_complete())
 97                                                {
 98                                                    editor
 99                                                        .update(cx, |editor, cx| {
100                                                            editor.run_command(
101                                                                command_range.clone(),
102                                                                &command_name,
103                                                                &[],
104                                                                true,
105                                                                workspace.clone(),
106                                                                window,
107                                                                cx,
108                                                            );
109                                                        })
110                                                        .ok();
111                                                    false
112                                                } else {
113                                                    requires_argument || accepts_arguments
114                                                }
115                                            },
116                                        ) as Arc<_>
117                                });
118
119                        Some(project::Completion {
120                            replace_range: name_range.clone(),
121                            documentation: Some(CompletionDocumentation::SingleLine(
122                                command.description().into(),
123                            )),
124                            new_text,
125                            label: command.label(cx),
126                            icon_path: None,
127                            insert_text_mode: None,
128                            confirm,
129                            source: CompletionSource::Custom,
130                        })
131                    })
132                    .collect();
133
134                vec![project::CompletionResponse {
135                    completions,
136                    is_incomplete: false,
137                }]
138            })
139        })
140    }
141
142    fn complete_command_argument(
143        &self,
144        command_name: &str,
145        arguments: &[String],
146        command_range: Range<Anchor>,
147        argument_range: Range<Anchor>,
148        last_argument_range: Range<Anchor>,
149        window: &mut Window,
150        cx: &mut App,
151    ) -> Task<Result<Vec<project::CompletionResponse>>> {
152        let new_cancel_flag = Arc::new(AtomicBool::new(false));
153        let mut flag = self.cancel_flag.lock();
154        flag.store(true, SeqCst);
155        *flag = new_cancel_flag.clone();
156        if let Some(command) = self.slash_commands.command(command_name, cx) {
157            let completions = command.complete_argument(
158                arguments,
159                new_cancel_flag,
160                self.workspace.clone(),
161                window,
162                cx,
163            );
164            let command_name: Arc<str> = command_name.into();
165            let editor = self.editor.clone();
166            let workspace = self.workspace.clone();
167            let arguments = arguments.to_vec();
168            cx.background_spawn(async move {
169                let completions = 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                Ok(vec![project::CompletionResponse {
239                    completions,
240                    // TODO: Could have slash commands indicate whether their completions are incomplete.
241                    is_incomplete: true,
242                }])
243            })
244        } else {
245            Task::ready(Ok(vec![project::CompletionResponse {
246                completions: Vec::new(),
247                is_incomplete: true,
248            }]))
249        }
250    }
251}
252
253impl CompletionProvider for SlashCommandCompletionProvider {
254    fn completions(
255        &self,
256        _excerpt_id: ExcerptId,
257        buffer: &Entity<Buffer>,
258        buffer_position: Anchor,
259        _: editor::CompletionContext,
260        window: &mut Window,
261        cx: &mut Context<Editor>,
262    ) -> Task<Result<Vec<project::CompletionResponse>>> {
263        let Some((name, arguments, command_range, last_argument_range)) =
264            buffer.update(cx, |buffer, _cx| {
265                let position = buffer_position.to_point(buffer);
266                let line_start = Point::new(position.row, 0);
267                let mut lines = buffer.text_for_range(line_start..position).lines();
268                let line = lines.next()?;
269                let call = SlashCommandLine::parse(line)?;
270
271                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
272                let command_range_end = Point::new(
273                    position.row,
274                    call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
275                );
276                let command_range = buffer.anchor_before(command_range_start)
277                    ..buffer.anchor_after(command_range_end);
278
279                let name = line[call.name.clone()].to_string();
280                let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
281                {
282                    let last_arg_start =
283                        buffer.anchor_before(Point::new(position.row, argument.start as u32));
284                    let first_arg_start = call.arguments.first().expect("we have the last element");
285                    let first_arg_start = buffer
286                        .anchor_before(Point::new(position.row, first_arg_start.start as u32));
287                    let arguments = call
288                        .arguments
289                        .into_iter()
290                        .filter_map(|argument| Some(line.get(argument)?.to_string()))
291                        .collect::<Vec<_>>();
292                    let argument_range = first_arg_start..buffer_position;
293                    (
294                        Some((arguments, argument_range)),
295                        last_arg_start..buffer_position,
296                    )
297                } else {
298                    let start =
299                        buffer.anchor_before(Point::new(position.row, call.name.start as u32));
300                    (None, start..buffer_position)
301                };
302
303                Some((name, arguments, command_range, last_argument_range))
304            })
305        else {
306            return Task::ready(Ok(vec![project::CompletionResponse {
307                completions: Vec::new(),
308                is_incomplete: false,
309            }]));
310        };
311
312        if let Some((arguments, argument_range)) = arguments {
313            self.complete_command_argument(
314                &name,
315                &arguments,
316                command_range,
317                argument_range,
318                last_argument_range,
319                window,
320                cx,
321            )
322        } else {
323            self.complete_command_name(&name, command_range, last_argument_range, window, cx)
324        }
325    }
326
327    fn is_completion_trigger(
328        &self,
329        buffer: &Entity<Buffer>,
330        position: language::Anchor,
331        _text: &str,
332        _trigger_in_words: bool,
333        _menu_is_open: bool,
334        cx: &mut Context<Editor>,
335    ) -> bool {
336        let buffer = buffer.read(cx);
337        let position = position.to_point(buffer);
338        let line_start = Point::new(position.row, 0);
339        let mut lines = buffer.text_for_range(line_start..position).lines();
340        if let Some(line) = lines.next() {
341            SlashCommandLine::parse(line).is_some()
342        } else {
343            false
344        }
345    }
346
347    fn sort_completions(&self) -> bool {
348        false
349    }
350}