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