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