slash_command.rs

  1use crate::assistant_panel::ConversationEditor;
  2use anyhow::Result;
  3pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
  4use editor::{CompletionProvider, Editor};
  5use fuzzy::{match_strings, StringMatchCandidate};
  6use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
  7use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
  8use parking_lot::{Mutex, RwLock};
  9use rope::Point;
 10use std::{
 11    ops::Range,
 12    sync::{
 13        atomic::{AtomicBool, Ordering::SeqCst},
 14        Arc,
 15    },
 16};
 17use workspace::Workspace;
 18
 19pub mod active_command;
 20pub mod file_command;
 21pub mod project_command;
 22pub mod prompt_command;
 23pub mod search_command;
 24pub mod tabs_command;
 25
 26pub(crate) struct SlashCommandCompletionProvider {
 27    editor: WeakView<ConversationEditor>,
 28    commands: Arc<SlashCommandRegistry>,
 29    cancel_flag: Mutex<Arc<AtomicBool>>,
 30    workspace: WeakView<Workspace>,
 31}
 32
 33pub(crate) struct SlashCommandLine {
 34    /// The range within the line containing the command name.
 35    pub name: Range<usize>,
 36    /// The range within the line containing the command argument.
 37    pub argument: Option<Range<usize>>,
 38}
 39
 40impl SlashCommandCompletionProvider {
 41    pub fn new(
 42        editor: WeakView<ConversationEditor>,
 43        commands: Arc<SlashCommandRegistry>,
 44        workspace: WeakView<Workspace>,
 45    ) -> Self {
 46        Self {
 47            cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
 48            editor,
 49            commands,
 50            workspace,
 51        }
 52    }
 53
 54    fn complete_command_name(
 55        &self,
 56        command_name: &str,
 57        command_range: Range<Anchor>,
 58        name_range: Range<Anchor>,
 59        cx: &mut WindowContext,
 60    ) -> Task<Result<Vec<project::Completion>>> {
 61        let candidates = self
 62            .commands
 63            .command_names()
 64            .into_iter()
 65            .enumerate()
 66            .map(|(ix, def)| StringMatchCandidate {
 67                id: ix,
 68                string: def.to_string(),
 69                char_bag: def.as_ref().into(),
 70            })
 71            .collect::<Vec<_>>();
 72        let commands = self.commands.clone();
 73        let command_name = command_name.to_string();
 74        let editor = self.editor.clone();
 75        let workspace = self.workspace.clone();
 76        cx.spawn(|mut cx| async move {
 77            let matches = match_strings(
 78                &candidates,
 79                &command_name,
 80                true,
 81                usize::MAX,
 82                &Default::default(),
 83                cx.background_executor().clone(),
 84            )
 85            .await;
 86
 87            cx.update(|cx| {
 88                matches
 89                    .into_iter()
 90                    .filter_map(|mat| {
 91                        let command = commands.command(&mat.string)?;
 92                        let mut new_text = mat.string.clone();
 93                        let requires_argument = command.requires_argument();
 94                        if requires_argument {
 95                            new_text.push(' ');
 96                        }
 97
 98                        Some(project::Completion {
 99                            old_range: name_range.clone(),
100                            documentation: Some(Documentation::SingleLine(command.description())),
101                            new_text,
102                            label: command.label(cx),
103                            server_id: LanguageServerId(0),
104                            lsp_completion: Default::default(),
105                            show_new_completions_on_confirm: requires_argument,
106                            confirm: (!requires_argument).then(|| {
107                                let command_name = mat.string.clone();
108                                let command_range = command_range.clone();
109                                let editor = editor.clone();
110                                let workspace = workspace.clone();
111                                Arc::new(move |cx: &mut WindowContext| {
112                                    editor
113                                        .update(cx, |editor, cx| {
114                                            editor.run_command(
115                                                command_range.clone(),
116                                                &command_name,
117                                                None,
118                                                workspace.clone(),
119                                                cx,
120                                            );
121                                        })
122                                        .ok();
123                                }) as Arc<_>
124                            }),
125                        })
126                    })
127                    .collect()
128            })
129        })
130    }
131
132    fn complete_command_argument(
133        &self,
134        command_name: &str,
135        argument: String,
136        command_range: Range<Anchor>,
137        argument_range: Range<Anchor>,
138        cx: &mut WindowContext,
139    ) -> Task<Result<Vec<project::Completion>>> {
140        let new_cancel_flag = Arc::new(AtomicBool::new(false));
141        let mut flag = self.cancel_flag.lock();
142        flag.store(true, SeqCst);
143        *flag = new_cancel_flag.clone();
144
145        if let Some(command) = self.commands.command(command_name) {
146            let completions = command.complete_argument(
147                argument,
148                new_cancel_flag.clone(),
149                self.workspace.clone(),
150                cx,
151            );
152            let command_name: Arc<str> = command_name.into();
153            let editor = self.editor.clone();
154            let workspace = self.workspace.clone();
155            cx.background_executor().spawn(async move {
156                Ok(completions
157                    .await?
158                    .into_iter()
159                    .map(|arg| project::Completion {
160                        old_range: argument_range.clone(),
161                        label: CodeLabel::plain(arg.clone(), None),
162                        new_text: arg.clone(),
163                        documentation: None,
164                        server_id: LanguageServerId(0),
165                        lsp_completion: Default::default(),
166                        show_new_completions_on_confirm: false,
167                        confirm: Some(Arc::new({
168                            let command_name = command_name.clone();
169                            let command_range = command_range.clone();
170                            let editor = editor.clone();
171                            let workspace = workspace.clone();
172                            move |cx| {
173                                editor
174                                    .update(cx, |editor, cx| {
175                                        editor.run_command(
176                                            command_range.clone(),
177                                            &command_name,
178                                            Some(&arg),
179                                            workspace.clone(),
180                                            cx,
181                                        );
182                                    })
183                                    .ok();
184                            }
185                        })),
186                    })
187                    .collect())
188            })
189        } else {
190            cx.background_executor()
191                .spawn(async move { Ok(Vec::new()) })
192        }
193    }
194}
195
196impl CompletionProvider for SlashCommandCompletionProvider {
197    fn completions(
198        &self,
199        buffer: &Model<Buffer>,
200        buffer_position: Anchor,
201        cx: &mut ViewContext<Editor>,
202    ) -> Task<Result<Vec<project::Completion>>> {
203        let Some((name, argument, command_range, argument_range)) =
204            buffer.update(cx, |buffer, _cx| {
205                let position = buffer_position.to_point(buffer);
206                let line_start = Point::new(position.row, 0);
207                let mut lines = buffer.text_for_range(line_start..position).lines();
208                let line = lines.next()?;
209                let call = SlashCommandLine::parse(line)?;
210
211                let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
212                let command_range_end = Point::new(
213                    position.row,
214                    call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
215                );
216                let command_range = buffer.anchor_after(command_range_start)
217                    ..buffer.anchor_after(command_range_end);
218
219                let name = line[call.name.clone()].to_string();
220
221                Some(if let Some(argument) = call.argument {
222                    let start =
223                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
224                    let argument = line[argument.clone()].to_string();
225                    (name, Some(argument), command_range, start..buffer_position)
226                } else {
227                    let start =
228                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
229                    (name, None, command_range, start..buffer_position)
230                })
231            })
232        else {
233            return Task::ready(Ok(Vec::new()));
234        };
235
236        if let Some(argument) = argument {
237            self.complete_command_argument(&name, argument, command_range, argument_range, cx)
238        } else {
239            self.complete_command_name(&name, command_range, argument_range, cx)
240        }
241    }
242
243    fn resolve_completions(
244        &self,
245        _: Model<Buffer>,
246        _: Vec<usize>,
247        _: Arc<RwLock<Box<[project::Completion]>>>,
248        _: &mut ViewContext<Editor>,
249    ) -> Task<Result<bool>> {
250        Task::ready(Ok(true))
251    }
252
253    fn apply_additional_edits_for_completion(
254        &self,
255        _: Model<Buffer>,
256        _: project::Completion,
257        _: bool,
258        _: &mut ViewContext<Editor>,
259    ) -> Task<Result<Option<language::Transaction>>> {
260        Task::ready(Ok(None))
261    }
262
263    fn is_completion_trigger(
264        &self,
265        buffer: &Model<Buffer>,
266        position: language::Anchor,
267        _text: &str,
268        _trigger_in_words: bool,
269        cx: &mut ViewContext<Editor>,
270    ) -> bool {
271        let buffer = buffer.read(cx);
272        let position = position.to_point(buffer);
273        let line_start = Point::new(position.row, 0);
274        let mut lines = buffer.text_for_range(line_start..position).lines();
275        if let Some(line) = lines.next() {
276            SlashCommandLine::parse(line).is_some()
277        } else {
278            false
279        }
280    }
281}
282
283impl SlashCommandLine {
284    pub(crate) fn parse(line: &str) -> Option<Self> {
285        let mut call: Option<Self> = None;
286        let mut ix = 0;
287        for c in line.chars() {
288            let next_ix = ix + c.len_utf8();
289            if let Some(call) = &mut call {
290                // The command arguments start at the first non-whitespace character
291                // after the command name, and continue until the end of the line.
292                if let Some(argument) = &mut call.argument {
293                    if (*argument).is_empty() && c.is_whitespace() {
294                        argument.start = next_ix;
295                    }
296                    argument.end = next_ix;
297                }
298                // The command name ends at the first whitespace character.
299                else if !call.name.is_empty() {
300                    if c.is_whitespace() {
301                        call.argument = Some(next_ix..next_ix);
302                    } else {
303                        call.name.end = next_ix;
304                    }
305                }
306                // The command name must begin with a letter.
307                else if c.is_alphabetic() {
308                    call.name.end = next_ix;
309                } else {
310                    return None;
311                }
312            }
313            // Commands start with a slash.
314            else if c == '/' {
315                call = Some(SlashCommandLine {
316                    name: next_ix..next_ix,
317                    argument: None,
318                });
319            }
320            // The line can't contain anything before the slash except for whitespace.
321            else if !c.is_whitespace() {
322                return None;
323            }
324            ix = next_ix;
325        }
326        call
327    }
328}