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 Arc::new(
92 move |intent: CompletionIntent,
93 window: &mut Window,
94 cx: &mut App| {
95 if !requires_argument
96 && (!accepts_arguments || intent.is_complete())
97 {
98 editor
99 .update(cx, |editor, cx| {
100 editor.run_command(
101 command_range.clone(),
102 &command_name,
103 &[],
104 true,
105 workspace.clone(),
106 window,
107 cx,
108 );
109 })
110 .ok();
111 false
112 } else {
113 requires_argument || accepts_arguments
114 }
115 },
116 ) as Arc<_>
117 });
118
119 Some(project::Completion {
120 replace_range: name_range.clone(),
121 documentation: Some(CompletionDocumentation::SingleLine(
122 command.description().into(),
123 )),
124 new_text,
125 label: command.label(cx),
126 icon_path: None,
127 insert_text_mode: None,
128 confirm,
129 source: CompletionSource::Custom,
130 })
131 })
132 .collect();
133
134 vec![project::CompletionResponse {
135 completions,
136 is_incomplete: false,
137 }]
138 })
139 })
140 }
141
142 fn complete_command_argument(
143 &self,
144 command_name: &str,
145 arguments: &[String],
146 command_range: Range<Anchor>,
147 argument_range: Range<Anchor>,
148 last_argument_range: Range<Anchor>,
149 window: &mut Window,
150 cx: &mut App,
151 ) -> Task<Result<Vec<project::CompletionResponse>>> {
152 let new_cancel_flag = Arc::new(AtomicBool::new(false));
153 let mut flag = self.cancel_flag.lock();
154 flag.store(true, SeqCst);
155 *flag = new_cancel_flag.clone();
156 if let Some(command) = self.slash_commands.command(command_name, cx) {
157 let completions = command.complete_argument(
158 arguments,
159 new_cancel_flag,
160 self.workspace.clone(),
161 window,
162 cx,
163 );
164 let command_name: Arc<str> = command_name.into();
165 let editor = self.editor.clone();
166 let workspace = self.workspace.clone();
167 let arguments = arguments.to_vec();
168 cx.background_spawn(async move {
169 let completions = 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 Ok(vec![project::CompletionResponse {
239 completions,
240 // TODO: Could have slash commands indicate whether their completions are incomplete.
241 is_incomplete: true,
242 }])
243 })
244 } else {
245 Task::ready(Ok(vec![project::CompletionResponse {
246 completions: Vec::new(),
247 is_incomplete: true,
248 }]))
249 }
250 }
251}
252
253impl CompletionProvider for SlashCommandCompletionProvider {
254 fn completions(
255 &self,
256 _excerpt_id: ExcerptId,
257 buffer: &Entity<Buffer>,
258 buffer_position: Anchor,
259 _: editor::CompletionContext,
260 window: &mut Window,
261 cx: &mut Context<Editor>,
262 ) -> Task<Result<Vec<project::CompletionResponse>>> {
263 let Some((name, arguments, command_range, last_argument_range)) =
264 buffer.update(cx, |buffer, _cx| {
265 let position = buffer_position.to_point(buffer);
266 let line_start = Point::new(position.row, 0);
267 let mut lines = buffer.text_for_range(line_start..position).lines();
268 let line = lines.next()?;
269 let call = SlashCommandLine::parse(line)?;
270
271 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
272 let command_range_end = Point::new(
273 position.row,
274 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
275 );
276 let command_range = buffer.anchor_before(command_range_start)
277 ..buffer.anchor_after(command_range_end);
278
279 let name = line[call.name.clone()].to_string();
280 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
281 {
282 let last_arg_start =
283 buffer.anchor_before(Point::new(position.row, argument.start as u32));
284 let first_arg_start = call.arguments.first().expect("we have the last element");
285 let first_arg_start = buffer
286 .anchor_before(Point::new(position.row, first_arg_start.start as u32));
287 let arguments = call
288 .arguments
289 .into_iter()
290 .filter_map(|argument| Some(line.get(argument)?.to_string()))
291 .collect::<Vec<_>>();
292 let argument_range = first_arg_start..buffer_position;
293 (
294 Some((arguments, argument_range)),
295 last_arg_start..buffer_position,
296 )
297 } else {
298 let start =
299 buffer.anchor_before(Point::new(position.row, call.name.start as u32));
300 (None, start..buffer_position)
301 };
302
303 Some((name, arguments, command_range, last_argument_range))
304 })
305 else {
306 return Task::ready(Ok(vec![project::CompletionResponse {
307 completions: Vec::new(),
308 is_incomplete: false,
309 }]));
310 };
311
312 if let Some((arguments, argument_range)) = arguments {
313 self.complete_command_argument(
314 &name,
315 &arguments,
316 command_range,
317 argument_range,
318 last_argument_range,
319 window,
320 cx,
321 )
322 } else {
323 self.complete_command_name(&name, command_range, last_argument_range, window, cx)
324 }
325 }
326
327 fn is_completion_trigger(
328 &self,
329 buffer: &Entity<Buffer>,
330 position: language::Anchor,
331 _text: &str,
332 _trigger_in_words: bool,
333 _menu_is_open: bool,
334 cx: &mut Context<Editor>,
335 ) -> bool {
336 let buffer = buffer.read(cx);
337 let position = position.to_point(buffer);
338 let line_start = Point::new(position.row, 0);
339 let mut lines = buffer.text_for_range(line_start..position).lines();
340 if let Some(line) = lines.next() {
341 SlashCommandLine::parse(line).is_some()
342 } else {
343 false
344 }
345 }
346
347 fn sort_completions(&self) -> bool {
348 false
349 }
350}