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<Vec<project::CompletionResponse>>> {
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 let completions = matches
75 .into_iter()
76 .filter_map(|mat| {
77 let command = slash_commands.command(&mat.string, cx)?;
78 let mut new_text = mat.string.clone();
79 let requires_argument = command.requires_argument();
80 let accepts_arguments = command.accepts_arguments();
81 if requires_argument || accepts_arguments {
82 new_text.push(' ');
83 }
84
85 let confirm =
86 editor
87 .clone()
88 .zip(workspace.clone())
89 .map(|(editor, workspace)| {
90 let command_name = mat.string.clone();
91 let command_range = command_range.clone();
92 let editor = editor.clone();
93 let workspace = workspace.clone();
94 Arc::new(
95 move |intent: CompletionIntent,
96 window: &mut Window,
97 cx: &mut App| {
98 if !requires_argument
99 && (!accepts_arguments || intent.is_complete())
100 {
101 editor
102 .update(cx, |editor, cx| {
103 editor.run_command(
104 command_range.clone(),
105 &command_name,
106 &[],
107 true,
108 workspace.clone(),
109 window,
110 cx,
111 );
112 })
113 .ok();
114 false
115 } else {
116 requires_argument || accepts_arguments
117 }
118 },
119 ) as Arc<_>
120 });
121
122 Some(project::Completion {
123 replace_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 insert_text_mode: None,
131 confirm,
132 source: CompletionSource::Custom,
133 })
134 })
135 .collect();
136
137 vec![project::CompletionResponse {
138 completions,
139 is_incomplete: false,
140 }]
141 })
142 })
143 }
144
145 fn complete_command_argument(
146 &self,
147 command_name: &str,
148 arguments: &[String],
149 command_range: Range<Anchor>,
150 argument_range: Range<Anchor>,
151 last_argument_range: Range<Anchor>,
152 window: &mut Window,
153 cx: &mut App,
154 ) -> Task<Result<Vec<project::CompletionResponse>>> {
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 if let Some(command) = self.slash_commands.command(command_name, cx) {
160 let completions = command.complete_argument(
161 arguments,
162 new_cancel_flag.clone(),
163 self.workspace.clone(),
164 window,
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_spawn(async move {
172 let completions = completions
173 .await?
174 .into_iter()
175 .map(|new_argument| {
176 let confirm =
177 editor
178 .clone()
179 .zip(workspace.clone())
180 .map(|(editor, workspace)| {
181 Arc::new({
182 let mut completed_arguments = arguments.clone();
183 if new_argument.replace_previous_arguments {
184 completed_arguments.clear();
185 } else {
186 completed_arguments.pop();
187 }
188 completed_arguments.push(new_argument.new_text.clone());
189
190 let command_range = command_range.clone();
191 let command_name = command_name.clone();
192 move |intent: CompletionIntent,
193 window: &mut Window,
194 cx: &mut App| {
195 if new_argument.after_completion.run()
196 || intent.is_complete()
197 {
198 editor
199 .update(cx, |editor, cx| {
200 editor.run_command(
201 command_range.clone(),
202 &command_name,
203 &completed_arguments,
204 true,
205 workspace.clone(),
206 window,
207 cx,
208 );
209 })
210 .ok();
211 false
212 } else {
213 !new_argument.after_completion.run()
214 }
215 }
216 }) as Arc<_>
217 });
218
219 let mut new_text = new_argument.new_text.clone();
220 if new_argument.after_completion == AfterCompletion::Continue {
221 new_text.push(' ');
222 }
223
224 project::Completion {
225 replace_range: if new_argument.replace_previous_arguments {
226 argument_range.clone()
227 } else {
228 last_argument_range.clone()
229 },
230 label: new_argument.label,
231 icon_path: None,
232 new_text,
233 documentation: None,
234 confirm,
235 insert_text_mode: None,
236 source: CompletionSource::Custom,
237 }
238 })
239 .collect();
240
241 Ok(vec![project::CompletionResponse {
242 completions,
243 is_incomplete: false,
244 }])
245 })
246 } else {
247 Task::ready(Ok(vec![project::CompletionResponse {
248 completions: Vec::new(),
249 is_incomplete: false,
250 }]))
251 }
252 }
253}
254
255impl CompletionProvider for SlashCommandCompletionProvider {
256 fn completions(
257 &self,
258 _excerpt_id: ExcerptId,
259 buffer: &Entity<Buffer>,
260 buffer_position: Anchor,
261 _: editor::CompletionContext,
262 window: &mut Window,
263 cx: &mut Context<Editor>,
264 ) -> Task<Result<Vec<project::CompletionResponse>>> {
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 .into_iter()
292 .filter_map(|argument| Some(line.get(argument)?.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![project::CompletionResponse {
309 completions: Vec::new(),
310 is_incomplete: false,
311 }]));
312 };
313
314 if let Some((arguments, argument_range)) = arguments {
315 self.complete_command_argument(
316 &name,
317 &arguments,
318 command_range,
319 argument_range,
320 last_argument_range,
321 window,
322 cx,
323 )
324 } else {
325 self.complete_command_name(&name, command_range, last_argument_range, window, cx)
326 }
327 }
328
329 fn resolve_completions(
330 &self,
331 _: Entity<Buffer>,
332 _: Vec<usize>,
333 _: Rc<RefCell<Box<[project::Completion]>>>,
334 _: &mut Context<Editor>,
335 ) -> Task<Result<bool>> {
336 Task::ready(Ok(true))
337 }
338
339 fn is_completion_trigger(
340 &self,
341 buffer: &Entity<Buffer>,
342 position: language::Anchor,
343 _text: &str,
344 _trigger_in_words: bool,
345 cx: &mut Context<Editor>,
346 ) -> bool {
347 let buffer = buffer.read(cx);
348 let position = position.to_point(buffer);
349 let line_start = Point::new(position.row, 0);
350 let mut lines = buffer.text_for_range(line_start..position).lines();
351 if let Some(line) = lines.next() {
352 SlashCommandLine::parse(line).is_some()
353 } else {
354 false
355 }
356 }
357
358 fn sort_completions(&self) -> bool {
359 false
360 }
361}