1use crate::context_editor::ContextEditor;
2use anyhow::Result;
3pub use assistant_slash_command::SlashCommand;
4use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
5use editor::{CompletionProvider, Editor, ExcerptId};
6use fuzzy::{StringMatchCandidate, match_strings};
7use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
8use language::{Anchor, Buffer, ToPoint};
9use parking_lot::Mutex;
10use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
11use rope::Point;
12use std::{
13 cell::RefCell,
14 ops::Range,
15 rc::Rc,
16 sync::{
17 Arc,
18 atomic::{AtomicBool, Ordering::SeqCst},
19 },
20};
21use workspace::Workspace;
22
23pub struct SlashCommandCompletionProvider {
24 cancel_flag: Mutex<Arc<AtomicBool>>,
25 slash_commands: Arc<SlashCommandWorkingSet>,
26 editor: Option<WeakEntity<ContextEditor>>,
27 workspace: Option<WeakEntity<Workspace>>,
28}
29
30impl SlashCommandCompletionProvider {
31 pub fn new(
32 slash_commands: Arc<SlashCommandWorkingSet>,
33 editor: Option<WeakEntity<ContextEditor>>,
34 workspace: Option<WeakEntity<Workspace>>,
35 ) -> Self {
36 Self {
37 cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
38 slash_commands,
39 editor,
40 workspace,
41 }
42 }
43
44 fn complete_command_name(
45 &self,
46 command_name: &str,
47 command_range: Range<Anchor>,
48 name_range: Range<Anchor>,
49 window: &mut Window,
50 cx: &mut App,
51 ) -> Task<Result<Option<Vec<project::Completion>>>> {
52 let slash_commands = self.slash_commands.clone();
53 let candidates = slash_commands
54 .command_names(cx)
55 .into_iter()
56 .enumerate()
57 .map(|(ix, def)| StringMatchCandidate::new(ix, &def))
58 .collect::<Vec<_>>();
59 let command_name = command_name.to_string();
60 let editor = self.editor.clone();
61 let workspace = self.workspace.clone();
62 window.spawn(cx, async move |cx| {
63 let matches = match_strings(
64 &candidates,
65 &command_name,
66 true,
67 usize::MAX,
68 &Default::default(),
69 cx.background_executor().clone(),
70 )
71 .await;
72
73 cx.update(|_, cx| {
74 Some(
75 matches
76 .into_iter()
77 .filter_map(|mat| {
78 let command = slash_commands.command(&mat.string, cx)?;
79 let mut new_text = mat.string.clone();
80 let requires_argument = command.requires_argument();
81 let accepts_arguments = command.accepts_arguments();
82 if requires_argument || accepts_arguments {
83 new_text.push(' ');
84 }
85
86 let confirm =
87 editor
88 .clone()
89 .zip(workspace.clone())
90 .map(|(editor, workspace)| {
91 let command_name = mat.string.clone();
92 let command_range = command_range.clone();
93 let editor = editor.clone();
94 let workspace = workspace.clone();
95 Arc::new(
96 move |intent: CompletionIntent,
97 window: &mut Window,
98 cx: &mut App| {
99 if !requires_argument
100 && (!accepts_arguments || intent.is_complete())
101 {
102 editor
103 .update(cx, |editor, cx| {
104 editor.run_command(
105 command_range.clone(),
106 &command_name,
107 &[],
108 true,
109 workspace.clone(),
110 window,
111 cx,
112 );
113 })
114 .ok();
115 false
116 } else {
117 requires_argument || accepts_arguments
118 }
119 },
120 ) as Arc<_>
121 });
122 Some(project::Completion {
123 old_range: name_range.clone(),
124 documentation: Some(CompletionDocumentation::SingleLine(
125 command.description().into(),
126 )),
127 new_text,
128 label: command.label(cx),
129 icon_path: None,
130 confirm,
131 source: CompletionSource::Custom,
132 })
133 })
134 .collect(),
135 )
136 })
137 })
138 }
139
140 fn complete_command_argument(
141 &self,
142 command_name: &str,
143 arguments: &[String],
144 command_range: Range<Anchor>,
145 argument_range: Range<Anchor>,
146 last_argument_range: Range<Anchor>,
147 window: &mut Window,
148 cx: &mut App,
149 ) -> Task<Result<Option<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 window,
160 cx,
161 );
162 let command_name: Arc<str> = command_name.into();
163 let editor = self.editor.clone();
164 let workspace = self.workspace.clone();
165 let arguments = arguments.to_vec();
166 cx.background_spawn(async move {
167 Ok(Some(
168 completions
169 .await?
170 .into_iter()
171 .map(|new_argument| {
172 let confirm =
173 editor
174 .clone()
175 .zip(workspace.clone())
176 .map(|(editor, workspace)| {
177 Arc::new({
178 let mut completed_arguments = arguments.clone();
179 if new_argument.replace_previous_arguments {
180 completed_arguments.clear();
181 } else {
182 completed_arguments.pop();
183 }
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 |intent: CompletionIntent,
189 window: &mut Window,
190 cx: &mut App| {
191 if new_argument.after_completion.run()
192 || intent.is_complete()
193 {
194 editor
195 .update(cx, |editor, cx| {
196 editor.run_command(
197 command_range.clone(),
198 &command_name,
199 &completed_arguments,
200 true,
201 workspace.clone(),
202 window,
203 cx,
204 );
205 })
206 .ok();
207 false
208 } else {
209 !new_argument.after_completion.run()
210 }
211 }
212 }) as Arc<_>
213 });
214
215 let mut new_text = new_argument.new_text.clone();
216 if new_argument.after_completion == AfterCompletion::Continue {
217 new_text.push(' ');
218 }
219
220 project::Completion {
221 old_range: if new_argument.replace_previous_arguments {
222 argument_range.clone()
223 } else {
224 last_argument_range.clone()
225 },
226 label: new_argument.label,
227 icon_path: None,
228 new_text,
229 documentation: None,
230 confirm,
231 source: CompletionSource::Custom,
232 }
233 })
234 .collect(),
235 ))
236 })
237 } else {
238 Task::ready(Ok(Some(Vec::new())))
239 }
240 }
241}
242
243impl CompletionProvider for SlashCommandCompletionProvider {
244 fn completions(
245 &self,
246 _excerpt_id: ExcerptId,
247 buffer: &Entity<Buffer>,
248 buffer_position: Anchor,
249 _: editor::CompletionContext,
250 window: &mut Window,
251 cx: &mut Context<Editor>,
252 ) -> Task<Result<Option<Vec<project::Completion>>>> {
253 let Some((name, arguments, command_range, last_argument_range)) =
254 buffer.update(cx, |buffer, _cx| {
255 let position = buffer_position.to_point(buffer);
256 let line_start = Point::new(position.row, 0);
257 let mut lines = buffer.text_for_range(line_start..position).lines();
258 let line = lines.next()?;
259 let call = SlashCommandLine::parse(line)?;
260
261 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
262 let command_range_end = Point::new(
263 position.row,
264 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
265 );
266 let command_range = buffer.anchor_after(command_range_start)
267 ..buffer.anchor_after(command_range_end);
268
269 let name = line[call.name.clone()].to_string();
270 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
271 {
272 let last_arg_start =
273 buffer.anchor_after(Point::new(position.row, argument.start as u32));
274 let first_arg_start = call.arguments.first().expect("we have the last element");
275 let first_arg_start =
276 buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
277 let arguments = call
278 .arguments
279 .iter()
280 .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
281 .collect::<Vec<_>>();
282 let argument_range = first_arg_start..buffer_position;
283 (
284 Some((arguments, argument_range)),
285 last_arg_start..buffer_position,
286 )
287 } else {
288 let start =
289 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
290 (None, start..buffer_position)
291 };
292
293 Some((name, arguments, command_range, last_argument_range))
294 })
295 else {
296 return Task::ready(Ok(Some(Vec::new())));
297 };
298
299 if let Some((arguments, argument_range)) = arguments {
300 self.complete_command_argument(
301 &name,
302 &arguments,
303 command_range,
304 argument_range,
305 last_argument_range,
306 window,
307 cx,
308 )
309 } else {
310 self.complete_command_name(&name, command_range, last_argument_range, window, cx)
311 }
312 }
313
314 fn resolve_completions(
315 &self,
316 _: Entity<Buffer>,
317 _: Vec<usize>,
318 _: Rc<RefCell<Box<[project::Completion]>>>,
319 _: &mut Context<Editor>,
320 ) -> Task<Result<bool>> {
321 Task::ready(Ok(true))
322 }
323
324 fn is_completion_trigger(
325 &self,
326 buffer: &Entity<Buffer>,
327 position: language::Anchor,
328 _text: &str,
329 _trigger_in_words: bool,
330 cx: &mut Context<Editor>,
331 ) -> bool {
332 let buffer = buffer.read(cx);
333 let position = position.to_point(buffer);
334 let line_start = Point::new(position.row, 0);
335 let mut lines = buffer.text_for_range(line_start..position).lines();
336 if let Some(line) = lines.next() {
337 SlashCommandLine::parse(line).is_some()
338 } else {
339 false
340 }
341 }
342
343 fn sort_completions(&self) -> bool {
344 false
345 }
346}