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