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