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