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