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