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 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 })
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 window: &mut Window,
149 cx: &mut App,
150 ) -> Task<Result<Option<Vec<project::Completion>>>> {
151 let new_cancel_flag = Arc::new(AtomicBool::new(false));
152 let mut flag = self.cancel_flag.lock();
153 flag.store(true, SeqCst);
154 *flag = new_cancel_flag.clone();
155 if let Some(command) = self.slash_commands.command(command_name, cx) {
156 let completions = command.complete_argument(
157 arguments,
158 new_cancel_flag.clone(),
159 self.workspace.clone(),
160 window,
161 cx,
162 );
163 let command_name: Arc<str> = command_name.into();
164 let editor = self.editor.clone();
165 let workspace = self.workspace.clone();
166 let arguments = arguments.to_vec();
167 cx.background_spawn(async move {
168 Ok(Some(
169 completions
170 .await?
171 .into_iter()
172 .map(|new_argument| {
173 let confirm =
174 editor
175 .clone()
176 .zip(workspace.clone())
177 .map(|(editor, workspace)| {
178 Arc::new({
179 let mut completed_arguments = arguments.clone();
180 if new_argument.replace_previous_arguments {
181 completed_arguments.clear();
182 } else {
183 completed_arguments.pop();
184 }
185 completed_arguments.push(new_argument.new_text.clone());
186
187 let command_range = command_range.clone();
188 let command_name = command_name.clone();
189 move |intent: CompletionIntent,
190 window: &mut Window,
191 cx: &mut App| {
192 if new_argument.after_completion.run()
193 || intent.is_complete()
194 {
195 editor
196 .update(cx, |editor, cx| {
197 editor.run_command(
198 command_range.clone(),
199 &command_name,
200 &completed_arguments,
201 true,
202 workspace.clone(),
203 window,
204 cx,
205 );
206 })
207 .ok();
208 false
209 } else {
210 !new_argument.after_completion.run()
211 }
212 }
213 }) as Arc<_>
214 });
215
216 let mut new_text = new_argument.new_text.clone();
217 if new_argument.after_completion == AfterCompletion::Continue {
218 new_text.push(' ');
219 }
220
221 project::Completion {
222 replace_range: if new_argument.replace_previous_arguments {
223 argument_range.clone()
224 } else {
225 last_argument_range.clone()
226 },
227 label: new_argument.label,
228 icon_path: None,
229 new_text,
230 documentation: None,
231 confirm,
232 insert_text_mode: None,
233 source: CompletionSource::Custom,
234 }
235 })
236 .collect(),
237 ))
238 })
239 } else {
240 Task::ready(Ok(Some(Vec::new())))
241 }
242 }
243}
244
245impl CompletionProvider for SlashCommandCompletionProvider {
246 fn completions(
247 &self,
248 _excerpt_id: ExcerptId,
249 buffer: &Entity<Buffer>,
250 buffer_position: Anchor,
251 _: editor::CompletionContext,
252 window: &mut Window,
253 cx: &mut Context<Editor>,
254 ) -> Task<Result<Option<Vec<project::Completion>>>> {
255 let Some((name, arguments, command_range, last_argument_range)) =
256 buffer.update(cx, |buffer, _cx| {
257 let position = buffer_position.to_point(buffer);
258 let line_start = Point::new(position.row, 0);
259 let mut lines = buffer.text_for_range(line_start..position).lines();
260 let line = lines.next()?;
261 let call = SlashCommandLine::parse(line)?;
262
263 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
264 let command_range_end = Point::new(
265 position.row,
266 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
267 );
268 let command_range = buffer.anchor_after(command_range_start)
269 ..buffer.anchor_after(command_range_end);
270
271 let name = line[call.name.clone()].to_string();
272 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
273 {
274 let last_arg_start =
275 buffer.anchor_after(Point::new(position.row, argument.start as u32));
276 let first_arg_start = call.arguments.first().expect("we have the last element");
277 let first_arg_start =
278 buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
279 let arguments = call
280 .arguments
281 .into_iter()
282 .filter_map(|argument| Some(line.get(argument)?.to_string()))
283 .collect::<Vec<_>>();
284 let argument_range = first_arg_start..buffer_position;
285 (
286 Some((arguments, argument_range)),
287 last_arg_start..buffer_position,
288 )
289 } else {
290 let start =
291 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
292 (None, start..buffer_position)
293 };
294
295 Some((name, arguments, command_range, last_argument_range))
296 })
297 else {
298 return Task::ready(Ok(Some(Vec::new())));
299 };
300
301 if let Some((arguments, argument_range)) = arguments {
302 self.complete_command_argument(
303 &name,
304 &arguments,
305 command_range,
306 argument_range,
307 last_argument_range,
308 window,
309 cx,
310 )
311 } else {
312 self.complete_command_name(&name, command_range, last_argument_range, window, cx)
313 }
314 }
315
316 fn resolve_completions(
317 &self,
318 _: Entity<Buffer>,
319 _: Vec<usize>,
320 _: Rc<RefCell<Box<[project::Completion]>>>,
321 _: &mut Context<Editor>,
322 ) -> Task<Result<bool>> {
323 Task::ready(Ok(true))
324 }
325
326 fn is_completion_trigger(
327 &self,
328 buffer: &Entity<Buffer>,
329 position: language::Anchor,
330 _text: &str,
331 _trigger_in_words: bool,
332 cx: &mut Context<Editor>,
333 ) -> bool {
334 let buffer = buffer.read(cx);
335 let position = position.to_point(buffer);
336 let line_start = Point::new(position.row, 0);
337 let mut lines = buffer.text_for_range(line_start..position).lines();
338 if let Some(line) = lines.next() {
339 SlashCommandLine::parse(line).is_some()
340 } else {
341 false
342 }
343 }
344
345 fn sort_completions(&self) -> bool {
346 false
347 }
348}