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