slash_command.rs

  1use anyhow::Result;
  2use collections::HashMap;
  3use editor::{CompletionProvider, Editor};
  4use futures::channel::oneshot;
  5use fuzzy::{match_strings, StringMatchCandidate};
  6use gpui::{AppContext, Model, Task, ViewContext, WindowHandle};
  7use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
  8use parking_lot::{Mutex, RwLock};
  9use project::Project;
 10use rope::Point;
 11use std::{
 12    ops::Range,
 13    sync::{
 14        atomic::{AtomicBool, Ordering::SeqCst},
 15        Arc,
 16    },
 17};
 18use workspace::Workspace;
 19
 20use crate::PromptLibrary;
 21
 22mod current_file_command;
 23mod file_command;
 24mod prompt_command;
 25
 26pub(crate) struct SlashCommandCompletionProvider {
 27    commands: Arc<SlashCommandRegistry>,
 28    cancel_flag: Mutex<Arc<AtomicBool>>,
 29}
 30
 31#[derive(Default)]
 32pub(crate) struct SlashCommandRegistry {
 33    commands: HashMap<String, Box<dyn SlashCommand>>,
 34}
 35
 36pub(crate) trait SlashCommand: 'static + Send + Sync {
 37    fn name(&self) -> String;
 38    fn description(&self) -> String;
 39    fn complete_argument(
 40        &self,
 41        query: String,
 42        cancel: Arc<AtomicBool>,
 43        cx: &mut AppContext,
 44    ) -> Task<Result<Vec<String>>>;
 45    fn requires_argument(&self) -> bool;
 46    fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation;
 47}
 48
 49pub(crate) struct SlashCommandInvocation {
 50    pub output: Task<Result<String>>,
 51    pub invalidated: oneshot::Receiver<()>,
 52    pub cleanup: SlashCommandCleanup,
 53}
 54
 55#[derive(Default)]
 56pub(crate) struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
 57
 58impl SlashCommandCleanup {
 59    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
 60        Self(Some(Box::new(cleanup)))
 61    }
 62}
 63
 64impl Drop for SlashCommandCleanup {
 65    fn drop(&mut self) {
 66        if let Some(cleanup) = self.0.take() {
 67            cleanup();
 68        }
 69    }
 70}
 71
 72pub(crate) struct SlashCommandLine {
 73    /// The range within the line containing the command name.
 74    pub name: Range<usize>,
 75    /// The range within the line containing the command argument.
 76    pub argument: Option<Range<usize>>,
 77}
 78
 79impl SlashCommandRegistry {
 80    pub fn new(
 81        project: Model<Project>,
 82        prompt_library: Arc<PromptLibrary>,
 83        window: Option<WindowHandle<Workspace>>,
 84    ) -> Arc<Self> {
 85        let mut this = Self {
 86            commands: HashMap::default(),
 87        };
 88
 89        this.register_command(file_command::FileSlashCommand::new(project));
 90        this.register_command(prompt_command::PromptSlashCommand::new(prompt_library));
 91        if let Some(window) = window {
 92            this.register_command(current_file_command::CurrentFileSlashCommand::new(window));
 93        }
 94
 95        Arc::new(this)
 96    }
 97
 98    fn register_command(&mut self, command: impl SlashCommand) {
 99        self.commands.insert(command.name(), Box::new(command));
100    }
101
102    fn command_names(&self) -> impl Iterator<Item = &String> {
103        self.commands.keys()
104    }
105
106    pub(crate) fn command(&self, name: &str) -> Option<&dyn SlashCommand> {
107        self.commands.get(name).map(|b| &**b)
108    }
109}
110
111impl SlashCommandCompletionProvider {
112    pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
113        Self {
114            cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
115            commands,
116        }
117    }
118
119    fn complete_command_name(
120        &self,
121        command_name: &str,
122        range: Range<Anchor>,
123        cx: &mut AppContext,
124    ) -> Task<Result<Vec<project::Completion>>> {
125        let candidates = self
126            .commands
127            .command_names()
128            .enumerate()
129            .map(|(ix, def)| StringMatchCandidate {
130                id: ix,
131                string: def.clone(),
132                char_bag: def.as_str().into(),
133            })
134            .collect::<Vec<_>>();
135        let commands = self.commands.clone();
136        let command_name = command_name.to_string();
137        let executor = cx.background_executor().clone();
138        executor.clone().spawn(async move {
139            let matches = match_strings(
140                &candidates,
141                &command_name,
142                true,
143                usize::MAX,
144                &Default::default(),
145                executor,
146            )
147            .await;
148
149            Ok(matches
150                .into_iter()
151                .filter_map(|mat| {
152                    let command = commands.command(&mat.string)?;
153                    let mut new_text = mat.string.clone();
154                    if command.requires_argument() {
155                        new_text.push(' ');
156                    }
157
158                    Some(project::Completion {
159                        old_range: range.clone(),
160                        documentation: Some(Documentation::SingleLine(command.description())),
161                        new_text,
162                        label: CodeLabel::plain(mat.string, None),
163                        server_id: LanguageServerId(0),
164                        lsp_completion: Default::default(),
165                    })
166                })
167                .collect())
168        })
169    }
170
171    fn complete_command_argument(
172        &self,
173        command_name: &str,
174        argument: String,
175        range: Range<Anchor>,
176        cx: &mut AppContext,
177    ) -> Task<Result<Vec<project::Completion>>> {
178        let new_cancel_flag = Arc::new(AtomicBool::new(false));
179        let mut flag = self.cancel_flag.lock();
180        flag.store(true, SeqCst);
181        *flag = new_cancel_flag.clone();
182
183        if let Some(command) = self.commands.command(command_name) {
184            let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
185            cx.background_executor().spawn(async move {
186                Ok(completions
187                    .await?
188                    .into_iter()
189                    .map(|arg| project::Completion {
190                        old_range: range.clone(),
191                        label: CodeLabel::plain(arg.clone(), None),
192                        new_text: arg.clone(),
193                        documentation: None,
194                        server_id: LanguageServerId(0),
195                        lsp_completion: Default::default(),
196                    })
197                    .collect())
198            })
199        } else {
200            cx.background_executor()
201                .spawn(async move { Ok(Vec::new()) })
202        }
203    }
204}
205
206impl CompletionProvider for SlashCommandCompletionProvider {
207    fn completions(
208        &self,
209        buffer: &Model<Buffer>,
210        buffer_position: Anchor,
211        cx: &mut ViewContext<Editor>,
212    ) -> Task<Result<Vec<project::Completion>>> {
213        let task = buffer.update(cx, |buffer, cx| {
214            let position = buffer_position.to_point(buffer);
215            let line_start = Point::new(position.row, 0);
216            let mut lines = buffer.text_for_range(line_start..position).lines();
217            let line = lines.next()?;
218            let call = SlashCommandLine::parse(line)?;
219
220            let name = &line[call.name.clone()];
221            if let Some(argument) = call.argument {
222                let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
223                let argument = line[argument.clone()].to_string();
224                Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
225            } else {
226                let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
227                Some(self.complete_command_name(name, start..buffer_position, cx))
228            }
229        });
230
231        task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
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}