slash_command.rs

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