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