1use crate::assistant_panel::ContextEditor;
2use anyhow::Result;
3pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
4use editor::{CompletionProvider, Editor};
5use fuzzy::{match_strings, StringMatchCandidate};
6use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
7use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
8use parking_lot::{Mutex, RwLock};
9use project::CompletionIntent;
10use rope::Point;
11use std::{
12 ops::Range,
13 sync::{
14 atomic::{AtomicBool, Ordering::SeqCst},
15 Arc,
16 },
17};
18use ui::ActiveTheme;
19use workspace::Workspace;
20
21pub mod context_server_command;
22pub mod default_command;
23pub mod diagnostics_command;
24pub mod docs_command;
25pub mod fetch_command;
26pub mod file_command;
27pub mod now_command;
28pub mod project_command;
29pub mod prompt_command;
30pub mod search_command;
31pub mod symbols_command;
32pub mod tab_command;
33pub mod terminal_command;
34pub mod workflow_command;
35
36pub(crate) struct SlashCommandCompletionProvider {
37 cancel_flag: Mutex<Arc<AtomicBool>>,
38 editor: Option<WeakView<ContextEditor>>,
39 workspace: Option<WeakView<Workspace>>,
40}
41
42pub(crate) struct SlashCommandLine {
43 /// The range within the line containing the command name.
44 pub name: Range<usize>,
45 /// Ranges within the line containing the command arguments.
46 pub arguments: Vec<Range<usize>>,
47}
48
49impl SlashCommandCompletionProvider {
50 pub fn new(
51 editor: Option<WeakView<ContextEditor>>,
52 workspace: Option<WeakView<Workspace>>,
53 ) -> Self {
54 Self {
55 cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
56 editor,
57 workspace,
58 }
59 }
60
61 fn complete_command_name(
62 &self,
63 command_name: &str,
64 command_range: Range<Anchor>,
65 name_range: Range<Anchor>,
66 cx: &mut WindowContext,
67 ) -> Task<Result<Vec<project::Completion>>> {
68 let commands = SlashCommandRegistry::global(cx);
69 let candidates = commands
70 .command_names()
71 .into_iter()
72 .enumerate()
73 .map(|(ix, def)| StringMatchCandidate {
74 id: ix,
75 string: def.to_string(),
76 char_bag: def.as_ref().into(),
77 })
78 .collect::<Vec<_>>();
79 let command_name = command_name.to_string();
80 let editor = self.editor.clone();
81 let workspace = self.workspace.clone();
82 cx.spawn(|mut cx| async move {
83 let matches = match_strings(
84 &candidates,
85 &command_name,
86 true,
87 usize::MAX,
88 &Default::default(),
89 cx.background_executor().clone(),
90 )
91 .await;
92
93 cx.update(|cx| {
94 matches
95 .into_iter()
96 .filter_map(|mat| {
97 let command = commands.command(&mat.string)?;
98 let mut new_text = mat.string.clone();
99 let requires_argument = command.requires_argument();
100 let accepts_arguments = command.accepts_arguments();
101 if requires_argument || accepts_arguments {
102 new_text.push(' ');
103 }
104
105 let confirm =
106 editor
107 .clone()
108 .zip(workspace.clone())
109 .map(|(editor, workspace)| {
110 let command_name = mat.string.clone();
111 let command_range = command_range.clone();
112 let editor = editor.clone();
113 let workspace = workspace.clone();
114 Arc::new(
115 move |intent: CompletionIntent, cx: &mut WindowContext| {
116 if !requires_argument
117 && (!accepts_arguments || intent.is_complete())
118 {
119 editor
120 .update(cx, |editor, cx| {
121 editor.run_command(
122 command_range.clone(),
123 &command_name,
124 &[],
125 true,
126 workspace.clone(),
127 cx,
128 );
129 })
130 .ok();
131 false
132 } else {
133 requires_argument || accepts_arguments
134 }
135 },
136 ) as Arc<_>
137 });
138 Some(project::Completion {
139 old_range: name_range.clone(),
140 documentation: Some(Documentation::SingleLine(command.description())),
141 new_text,
142 label: command.label(cx),
143 server_id: LanguageServerId(0),
144 lsp_completion: Default::default(),
145 confirm,
146 })
147 })
148 .collect()
149 })
150 })
151 }
152
153 fn complete_command_argument(
154 &self,
155 command_name: &str,
156 arguments: &[String],
157 command_range: Range<Anchor>,
158 argument_range: Range<Anchor>,
159 last_argument_range: Range<Anchor>,
160 cx: &mut WindowContext,
161 ) -> Task<Result<Vec<project::Completion>>> {
162 let new_cancel_flag = Arc::new(AtomicBool::new(false));
163 let mut flag = self.cancel_flag.lock();
164 flag.store(true, SeqCst);
165 *flag = new_cancel_flag.clone();
166 let commands = SlashCommandRegistry::global(cx);
167 if let Some(command) = commands.command(command_name) {
168 let completions = command.complete_argument(
169 arguments,
170 new_cancel_flag.clone(),
171 self.workspace.clone(),
172 cx,
173 );
174 let command_name: Arc<str> = command_name.into();
175 let editor = self.editor.clone();
176 let workspace = self.workspace.clone();
177 let arguments = arguments.to_vec();
178 cx.background_executor().spawn(async move {
179 Ok(completions
180 .await?
181 .into_iter()
182 .map(|new_argument| {
183 let confirm =
184 editor
185 .clone()
186 .zip(workspace.clone())
187 .map(|(editor, workspace)| {
188 Arc::new({
189 let mut completed_arguments = arguments.clone();
190 if new_argument.replace_previous_arguments {
191 completed_arguments.clear();
192 } else {
193 completed_arguments.pop();
194 }
195 completed_arguments.push(new_argument.new_text.clone());
196
197 let command_range = command_range.clone();
198 let command_name = command_name.clone();
199 move |intent: CompletionIntent, cx: &mut WindowContext| {
200 if new_argument.run_command || intent.is_complete() {
201 editor
202 .update(cx, |editor, cx| {
203 editor.run_command(
204 command_range.clone(),
205 &command_name,
206 &completed_arguments,
207 true,
208 workspace.clone(),
209 cx,
210 );
211 })
212 .ok();
213 false
214 } else {
215 !new_argument.run_command
216 }
217 }
218 }) as Arc<_>
219 });
220
221 let mut new_text = new_argument.new_text.clone();
222 if !new_argument.run_command {
223 new_text.push(' ');
224 }
225
226 project::Completion {
227 old_range: if new_argument.replace_previous_arguments {
228 argument_range.clone()
229 } else {
230 last_argument_range.clone()
231 },
232 label: new_argument.label,
233 new_text,
234 documentation: None,
235 server_id: LanguageServerId(0),
236 lsp_completion: Default::default(),
237 confirm,
238 }
239 })
240 .collect())
241 })
242 } else {
243 Task::ready(Ok(Vec::new()))
244 }
245 }
246}
247
248impl CompletionProvider for SlashCommandCompletionProvider {
249 fn completions(
250 &self,
251 buffer: &Model<Buffer>,
252 buffer_position: Anchor,
253 _: editor::CompletionContext,
254 cx: &mut ViewContext<Editor>,
255 ) -> Task<Result<Vec<project::Completion>>> {
256 let Some((name, arguments, command_range, last_argument_range)) =
257 buffer.update(cx, |buffer, _cx| {
258 let position = buffer_position.to_point(buffer);
259 let line_start = Point::new(position.row, 0);
260 let mut lines = buffer.text_for_range(line_start..position).lines();
261 let line = lines.next()?;
262 let call = SlashCommandLine::parse(line)?;
263
264 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
265 let command_range_end = Point::new(
266 position.row,
267 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
268 );
269 let command_range = buffer.anchor_after(command_range_start)
270 ..buffer.anchor_after(command_range_end);
271
272 let name = line[call.name.clone()].to_string();
273 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
274 {
275 let last_arg_start =
276 buffer.anchor_after(Point::new(position.row, argument.start as u32));
277 let first_arg_start = call.arguments.first().expect("we have the last element");
278 let first_arg_start =
279 buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
280 let arguments = call
281 .arguments
282 .iter()
283 .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
284 .collect::<Vec<_>>();
285 let argument_range = first_arg_start..buffer_position;
286 (
287 Some((arguments, argument_range)),
288 last_arg_start..buffer_position,
289 )
290 } else {
291 let start =
292 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
293 (None, start..buffer_position)
294 };
295
296 Some((name, arguments, command_range, last_argument_range))
297 })
298 else {
299 return Task::ready(Ok(Vec::new()));
300 };
301
302 if let Some((arguments, argument_range)) = arguments {
303 self.complete_command_argument(
304 &name,
305 &arguments,
306 command_range,
307 argument_range,
308 last_argument_range,
309 cx,
310 )
311 } else {
312 self.complete_command_name(&name, command_range, last_argument_range, cx)
313 }
314 }
315
316 fn resolve_completions(
317 &self,
318 _: Model<Buffer>,
319 _: Vec<usize>,
320 _: Arc<RwLock<Box<[project::Completion]>>>,
321 _: &mut ViewContext<Editor>,
322 ) -> Task<Result<bool>> {
323 Task::ready(Ok(true))
324 }
325
326 fn apply_additional_edits_for_completion(
327 &self,
328 _: Model<Buffer>,
329 _: project::Completion,
330 _: bool,
331 _: &mut ViewContext<Editor>,
332 ) -> Task<Result<Option<language::Transaction>>> {
333 Task::ready(Ok(None))
334 }
335
336 fn is_completion_trigger(
337 &self,
338 buffer: &Model<Buffer>,
339 position: language::Anchor,
340 _text: &str,
341 _trigger_in_words: bool,
342 cx: &mut ViewContext<Editor>,
343 ) -> bool {
344 let buffer = buffer.read(cx);
345 let position = position.to_point(buffer);
346 let line_start = Point::new(position.row, 0);
347 let mut lines = buffer.text_for_range(line_start..position).lines();
348 if let Some(line) = lines.next() {
349 SlashCommandLine::parse(line).is_some()
350 } else {
351 false
352 }
353 }
354
355 fn sort_completions(&self) -> bool {
356 false
357 }
358}
359
360impl SlashCommandLine {
361 pub(crate) fn parse(line: &str) -> Option<Self> {
362 let mut call: Option<Self> = None;
363 let mut ix = 0;
364 for c in line.chars() {
365 let next_ix = ix + c.len_utf8();
366 if let Some(call) = &mut call {
367 // The command arguments start at the first non-whitespace character
368 // after the command name, and continue until the end of the line.
369 if let Some(argument) = call.arguments.last_mut() {
370 if c.is_whitespace() {
371 if (*argument).is_empty() {
372 argument.start = next_ix;
373 argument.end = next_ix;
374 } else {
375 argument.end = ix;
376 call.arguments.push(next_ix..next_ix);
377 }
378 } else {
379 argument.end = next_ix;
380 }
381 }
382 // The command name ends at the first whitespace character.
383 else if !call.name.is_empty() {
384 if c.is_whitespace() {
385 call.arguments = vec![next_ix..next_ix];
386 } else {
387 call.name.end = next_ix;
388 }
389 }
390 // The command name must begin with a letter.
391 else if c.is_alphabetic() {
392 call.name.end = next_ix;
393 } else {
394 return None;
395 }
396 }
397 // Commands start with a slash.
398 else if c == '/' {
399 call = Some(SlashCommandLine {
400 name: next_ix..next_ix,
401 arguments: Vec::new(),
402 });
403 }
404 // The line can't contain anything before the slash except for whitespace.
405 else if !c.is_whitespace() {
406 return None;
407 }
408 ix = next_ix;
409 }
410 call
411 }
412}
413
414pub fn create_label_for_command(
415 command_name: &str,
416 arguments: &[&str],
417 cx: &AppContext,
418) -> CodeLabel {
419 let mut label = CodeLabel::default();
420 label.push_str(command_name, None);
421 label.push_str(" ", None);
422 label.push_str(
423 &arguments.join(" "),
424 cx.theme().syntax().highlight_id("comment").map(HighlightId),
425 );
426 label.filter_range = 0..command_name.len();
427 label
428}