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::{Model, Task, ViewContext, WeakView, WindowContext};
  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<WeakView<ContextEditor>>,
 27    workspace: Option<WeakView<Workspace>>,
 28}
 29
 30impl SlashCommandCompletionProvider {
 31    pub fn new(
 32        slash_commands: Arc<SlashCommandWorkingSet>,
 33        editor: Option<WeakView<ContextEditor>>,
 34        workspace: Option<WeakView<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        cx: &mut WindowContext,
 50    ) -> Task<Result<Vec<project::Completion>>> {
 51        let slash_commands = self.slash_commands.clone();
 52        let candidates = slash_commands
 53            .command_names(cx)
 54            .into_iter()
 55            .enumerate()
 56            .map(|(ix, def)| StringMatchCandidate::new(ix, &def))
 57            .collect::<Vec<_>>();
 58        let command_name = command_name.to_string();
 59        let editor = self.editor.clone();
 60        let workspace = self.workspace.clone();
 61        cx.spawn(|mut cx| async move {
 62            let matches = match_strings(
 63                &candidates,
 64                &command_name,
 65                true,
 66                usize::MAX,
 67                &Default::default(),
 68                cx.background_executor().clone(),
 69            )
 70            .await;
 71
 72            cx.update(|cx| {
 73                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                                    let editor = editor.clone();
 92                                    let workspace = workspace.clone();
 93                                    Arc::new(
 94                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
 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                                                            cx,
107                                                        );
108                                                    })
109                                                    .ok();
110                                                false
111                                            } else {
112                                                requires_argument || accepts_arguments
113                                            }
114                                        },
115                                    ) as Arc<_>
116                                });
117                        Some(project::Completion {
118                            old_range: name_range.clone(),
119                            documentation: Some(Documentation::SingleLine(command.description())),
120                            new_text,
121                            label: command.label(cx),
122                            server_id: LanguageServerId(0),
123                            lsp_completion: Default::default(),
124                            confirm,
125                            resolved: true,
126                        })
127                    })
128                    .collect()
129            })
130        })
131    }
132
133    fn complete_command_argument(
134        &self,
135        command_name: &str,
136        arguments: &[String],
137        command_range: Range<Anchor>,
138        argument_range: Range<Anchor>,
139        last_argument_range: Range<Anchor>,
140        cx: &mut WindowContext,
141    ) -> Task<Result<Vec<project::Completion>>> {
142        let new_cancel_flag = Arc::new(AtomicBool::new(false));
143        let mut flag = self.cancel_flag.lock();
144        flag.store(true, SeqCst);
145        *flag = new_cancel_flag.clone();
146        if let Some(command) = self.slash_commands.command(command_name, cx) {
147            let completions = command.complete_argument(
148                arguments,
149                new_cancel_flag.clone(),
150                self.workspace.clone(),
151                cx,
152            );
153            let command_name: Arc<str> = command_name.into();
154            let editor = self.editor.clone();
155            let workspace = self.workspace.clone();
156            let arguments = arguments.to_vec();
157            cx.background_executor().spawn(async move {
158                Ok(completions
159                    .await?
160                    .into_iter()
161                    .map(|new_argument| {
162                        let confirm =
163                            editor
164                                .clone()
165                                .zip(workspace.clone())
166                                .map(|(editor, workspace)| {
167                                    Arc::new({
168                                        let mut completed_arguments = arguments.clone();
169                                        if new_argument.replace_previous_arguments {
170                                            completed_arguments.clear();
171                                        } else {
172                                            completed_arguments.pop();
173                                        }
174                                        completed_arguments.push(new_argument.new_text.clone());
175
176                                        let command_range = command_range.clone();
177                                        let command_name = command_name.clone();
178                                        move |intent: CompletionIntent, cx: &mut WindowContext| {
179                                            if new_argument.after_completion.run()
180                                                || intent.is_complete()
181                                            {
182                                                editor
183                                                    .update(cx, |editor, cx| {
184                                                        editor.run_command(
185                                                            command_range.clone(),
186                                                            &command_name,
187                                                            &completed_arguments,
188                                                            true,
189                                                            workspace.clone(),
190                                                            cx,
191                                                        );
192                                                    })
193                                                    .ok();
194                                                false
195                                            } else {
196                                                !new_argument.after_completion.run()
197                                            }
198                                        }
199                                    }) as Arc<_>
200                                });
201
202                        let mut new_text = new_argument.new_text.clone();
203                        if new_argument.after_completion == AfterCompletion::Continue {
204                            new_text.push(' ');
205                        }
206
207                        project::Completion {
208                            old_range: if new_argument.replace_previous_arguments {
209                                argument_range.clone()
210                            } else {
211                                last_argument_range.clone()
212                            },
213                            label: new_argument.label,
214                            new_text,
215                            documentation: None,
216                            server_id: LanguageServerId(0),
217                            lsp_completion: Default::default(),
218                            confirm,
219                            resolved: true,
220                        }
221                    })
222                    .collect())
223            })
224        } else {
225            Task::ready(Ok(Vec::new()))
226        }
227    }
228}
229
230impl CompletionProvider for SlashCommandCompletionProvider {
231    fn completions(
232        &self,
233        buffer: &Model<Buffer>,
234        buffer_position: Anchor,
235        _: editor::CompletionContext,
236        cx: &mut ViewContext<Editor>,
237    ) -> Task<Result<Vec<project::Completion>>> {
238        let Some((name, arguments, command_range, last_argument_range)) =
239            buffer.update(cx, |buffer, _cx| {
240                let position = buffer_position.to_point(buffer);
241                let line_start = Point::new(position.row, 0);
242                let mut lines = buffer.text_for_range(line_start..position).lines();
243                let line = lines.next()?;
244                let call = SlashCommandLine::parse(line)?;
245
246                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
247                let command_range_end = Point::new(
248                    position.row,
249                    call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
250                );
251                let command_range = buffer.anchor_after(command_range_start)
252                    ..buffer.anchor_after(command_range_end);
253
254                let name = line[call.name.clone()].to_string();
255                let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
256                {
257                    let last_arg_start =
258                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
259                    let first_arg_start = call.arguments.first().expect("we have the last element");
260                    let first_arg_start =
261                        buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
262                    let arguments = call
263                        .arguments
264                        .iter()
265                        .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
266                        .collect::<Vec<_>>();
267                    let argument_range = first_arg_start..buffer_position;
268                    (
269                        Some((arguments, argument_range)),
270                        last_arg_start..buffer_position,
271                    )
272                } else {
273                    let start =
274                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
275                    (None, start..buffer_position)
276                };
277
278                Some((name, arguments, command_range, last_argument_range))
279            })
280        else {
281            return Task::ready(Ok(Vec::new()));
282        };
283
284        if let Some((arguments, argument_range)) = arguments {
285            self.complete_command_argument(
286                &name,
287                &arguments,
288                command_range,
289                argument_range,
290                last_argument_range,
291                cx,
292            )
293        } else {
294            self.complete_command_name(&name, command_range, last_argument_range, cx)
295        }
296    }
297
298    fn resolve_completions(
299        &self,
300        _: Model<Buffer>,
301        _: Vec<usize>,
302        _: Rc<RefCell<Box<[project::Completion]>>>,
303        _: &mut ViewContext<Editor>,
304    ) -> Task<Result<bool>> {
305        Task::ready(Ok(true))
306    }
307
308    fn is_completion_trigger(
309        &self,
310        buffer: &Model<Buffer>,
311        position: language::Anchor,
312        _text: &str,
313        _trigger_in_words: bool,
314        cx: &mut ViewContext<Editor>,
315    ) -> bool {
316        let buffer = buffer.read(cx);
317        let position = position.to_point(buffer);
318        let line_start = Point::new(position.row, 0);
319        let mut lines = buffer.text_for_range(line_start..position).lines();
320        if let Some(line) = lines.next() {
321            SlashCommandLine::parse(line).is_some()
322        } else {
323            false
324        }
325    }
326
327    fn sort_completions(&self) -> bool {
328        false
329    }
330}