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}