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 // TODO: Could have slash commands indicate whether their completions are incomplete.
242 is_incomplete: true,
243 }])
244 })
245 } else {
246 Task::ready(Ok(vec![project::CompletionResponse {
247 completions: Vec::new(),
248 is_incomplete: true,
249 }]))
250 }
251 }
252}
253
254impl CompletionProvider for SlashCommandCompletionProvider {
255 fn completions(
256 &self,
257 _excerpt_id: ExcerptId,
258 buffer: &Entity<Buffer>,
259 buffer_position: Anchor,
260 _: editor::CompletionContext,
261 window: &mut Window,
262 cx: &mut Context<Editor>,
263 ) -> Task<Result<Vec<project::CompletionResponse>>> {
264 let Some((name, arguments, command_range, last_argument_range)) =
265 buffer.update(cx, |buffer, _cx| {
266 let position = buffer_position.to_point(buffer);
267 let line_start = Point::new(position.row, 0);
268 let mut lines = buffer.text_for_range(line_start..position).lines();
269 let line = lines.next()?;
270 let call = SlashCommandLine::parse(line)?;
271
272 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
273 let command_range_end = Point::new(
274 position.row,
275 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
276 );
277 let command_range = buffer.anchor_before(command_range_start)
278 ..buffer.anchor_after(command_range_end);
279
280 let name = line[call.name.clone()].to_string();
281 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
282 {
283 let last_arg_start =
284 buffer.anchor_before(Point::new(position.row, argument.start as u32));
285 let first_arg_start = call.arguments.first().expect("we have the last element");
286 let first_arg_start = buffer
287 .anchor_before(Point::new(position.row, first_arg_start.start as u32));
288 let arguments = call
289 .arguments
290 .into_iter()
291 .filter_map(|argument| Some(line.get(argument)?.to_string()))
292 .collect::<Vec<_>>();
293 let argument_range = first_arg_start..buffer_position;
294 (
295 Some((arguments, argument_range)),
296 last_arg_start..buffer_position,
297 )
298 } else {
299 let start =
300 buffer.anchor_before(Point::new(position.row, call.name.start as u32));
301 (None, start..buffer_position)
302 };
303
304 Some((name, arguments, command_range, last_argument_range))
305 })
306 else {
307 return Task::ready(Ok(vec![project::CompletionResponse {
308 completions: Vec::new(),
309 is_incomplete: false,
310 }]));
311 };
312
313 if let Some((arguments, argument_range)) = arguments {
314 self.complete_command_argument(
315 &name,
316 &arguments,
317 command_range,
318 argument_range,
319 last_argument_range,
320 window,
321 cx,
322 )
323 } else {
324 self.complete_command_name(&name, command_range, last_argument_range, window, cx)
325 }
326 }
327
328 fn is_completion_trigger(
329 &self,
330 buffer: &Entity<Buffer>,
331 position: language::Anchor,
332 _text: &str,
333 _trigger_in_words: bool,
334 _menu_is_open: bool,
335 cx: &mut Context<Editor>,
336 ) -> bool {
337 let buffer = buffer.read(cx);
338 let position = position.to_point(buffer);
339 let line_start = Point::new(position.row, 0);
340 let mut lines = buffer.text_for_range(line_start..position).lines();
341 if let Some(line) = lines.next() {
342 SlashCommandLine::parse(line).is_some()
343 } else {
344 false
345 }
346 }
347
348 fn sort_completions(&self) -> bool {
349 false
350 }
351}