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