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