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