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