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