1use crate::assistant_panel::ContextEditor;
2use crate::SlashCommandWorkingSet;
3use anyhow::Result;
4use assistant_slash_command::AfterCompletion;
5pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
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::new(ix, &def))
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 = slash_commands.command(&mat.string, cx)?;
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 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 if let Some(command) = self.slash_commands.command(command_name, cx) {
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 workspace.clone(),
214 cx,
215 );
216 })
217 .ok();
218 false
219 } else {
220 !new_argument.after_completion.run()
221 }
222 }
223 }) as Arc<_>
224 });
225
226 let mut new_text = new_argument.new_text.clone();
227 if new_argument.after_completion == AfterCompletion::Continue {
228 new_text.push(' ');
229 }
230
231 project::Completion {
232 old_range: if new_argument.replace_previous_arguments {
233 argument_range.clone()
234 } else {
235 last_argument_range.clone()
236 },
237 label: new_argument.label,
238 new_text,
239 documentation: None,
240 server_id: LanguageServerId(0),
241 lsp_completion: Default::default(),
242 confirm,
243 }
244 })
245 .collect())
246 })
247 } else {
248 Task::ready(Ok(Vec::new()))
249 }
250 }
251}
252
253impl CompletionProvider for SlashCommandCompletionProvider {
254 fn completions(
255 &self,
256 buffer: &Model<Buffer>,
257 buffer_position: Anchor,
258 _: editor::CompletionContext,
259 cx: &mut ViewContext<Editor>,
260 ) -> Task<Result<Vec<project::Completion>>> {
261 let Some((name, arguments, command_range, last_argument_range)) =
262 buffer.update(cx, |buffer, _cx| {
263 let position = buffer_position.to_point(buffer);
264 let line_start = Point::new(position.row, 0);
265 let mut lines = buffer.text_for_range(line_start..position).lines();
266 let line = lines.next()?;
267 let call = SlashCommandLine::parse(line)?;
268
269 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
270 let command_range_end = Point::new(
271 position.row,
272 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
273 );
274 let command_range = buffer.anchor_after(command_range_start)
275 ..buffer.anchor_after(command_range_end);
276
277 let name = line[call.name.clone()].to_string();
278 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
279 {
280 let last_arg_start =
281 buffer.anchor_after(Point::new(position.row, argument.start as u32));
282 let first_arg_start = call.arguments.first().expect("we have the last element");
283 let first_arg_start =
284 buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
285 let arguments = call
286 .arguments
287 .iter()
288 .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
289 .collect::<Vec<_>>();
290 let argument_range = first_arg_start..buffer_position;
291 (
292 Some((arguments, argument_range)),
293 last_arg_start..buffer_position,
294 )
295 } else {
296 let start =
297 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
298 (None, start..buffer_position)
299 };
300
301 Some((name, arguments, command_range, last_argument_range))
302 })
303 else {
304 return Task::ready(Ok(Vec::new()));
305 };
306
307 if let Some((arguments, argument_range)) = arguments {
308 self.complete_command_argument(
309 &name,
310 &arguments,
311 command_range,
312 argument_range,
313 last_argument_range,
314 cx,
315 )
316 } else {
317 self.complete_command_name(&name, command_range, last_argument_range, cx)
318 }
319 }
320
321 fn resolve_completions(
322 &self,
323 _: Model<Buffer>,
324 _: Vec<usize>,
325 _: Arc<RwLock<Box<[project::Completion]>>>,
326 _: &mut ViewContext<Editor>,
327 ) -> Task<Result<bool>> {
328 Task::ready(Ok(true))
329 }
330
331 fn apply_additional_edits_for_completion(
332 &self,
333 _: Model<Buffer>,
334 _: project::Completion,
335 _: bool,
336 _: &mut ViewContext<Editor>,
337 ) -> Task<Result<Option<language::Transaction>>> {
338 Task::ready(Ok(None))
339 }
340
341 fn is_completion_trigger(
342 &self,
343 buffer: &Model<Buffer>,
344 position: language::Anchor,
345 _text: &str,
346 _trigger_in_words: bool,
347 cx: &mut ViewContext<Editor>,
348 ) -> bool {
349 let buffer = buffer.read(cx);
350 let position = position.to_point(buffer);
351 let line_start = Point::new(position.row, 0);
352 let mut lines = buffer.text_for_range(line_start..position).lines();
353 if let Some(line) = lines.next() {
354 SlashCommandLine::parse(line).is_some()
355 } else {
356 false
357 }
358 }
359
360 fn sort_completions(&self) -> bool {
361 false
362 }
363}
364
365impl SlashCommandLine {
366 pub(crate) fn parse(line: &str) -> Option<Self> {
367 let mut call: Option<Self> = None;
368 let mut ix = 0;
369 for c in line.chars() {
370 let next_ix = ix + c.len_utf8();
371 if let Some(call) = &mut call {
372 // The command arguments start at the first non-whitespace character
373 // after the command name, and continue until the end of the line.
374 if let Some(argument) = call.arguments.last_mut() {
375 if c.is_whitespace() {
376 if (*argument).is_empty() {
377 argument.start = next_ix;
378 argument.end = next_ix;
379 } else {
380 argument.end = ix;
381 call.arguments.push(next_ix..next_ix);
382 }
383 } else {
384 argument.end = next_ix;
385 }
386 }
387 // The command name ends at the first whitespace character.
388 else if !call.name.is_empty() {
389 if c.is_whitespace() {
390 call.arguments = vec![next_ix..next_ix];
391 } else {
392 call.name.end = next_ix;
393 }
394 }
395 // The command name must begin with a letter.
396 else if c.is_alphabetic() {
397 call.name.end = next_ix;
398 } else {
399 return None;
400 }
401 }
402 // Commands start with a slash.
403 else if c == '/' {
404 call = Some(SlashCommandLine {
405 name: next_ix..next_ix,
406 arguments: Vec::new(),
407 });
408 }
409 // The line can't contain anything before the slash except for whitespace.
410 else if !c.is_whitespace() {
411 return None;
412 }
413 ix = next_ix;
414 }
415 call
416 }
417}
418
419pub fn create_label_for_command(
420 command_name: &str,
421 arguments: &[&str],
422 cx: &AppContext,
423) -> CodeLabel {
424 let mut label = CodeLabel::default();
425 label.push_str(command_name, None);
426 label.push_str(" ", None);
427 label.push_str(
428 &arguments.join(" "),
429 cx.theme().syntax().highlight_id("comment").map(HighlightId),
430 );
431 label.filter_range = 0..command_name.len();
432 label
433}