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