1use crate::context_editor::ContextEditor;
2use anyhow::Result;
3pub use assistant_slash_command::SlashCommand;
4use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
5use editor::{CompletionProvider, Editor};
6use fuzzy::{match_strings, StringMatchCandidate};
7use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
8use language::{Anchor, Buffer, Documentation, LanguageServerId, ToPoint};
9use parking_lot::Mutex;
10use project::CompletionIntent;
11use rope::Point;
12use std::{
13 cell::RefCell,
14 ops::Range,
15 rc::Rc,
16 sync::{
17 atomic::{AtomicBool, Ordering::SeqCst},
18 Arc,
19 },
20};
21use workspace::Workspace;
22
23pub struct SlashCommandCompletionProvider {
24 cancel_flag: Mutex<Arc<AtomicBool>>,
25 slash_commands: Arc<SlashCommandWorkingSet>,
26 editor: Option<WeakView<ContextEditor>>,
27 workspace: Option<WeakView<Workspace>>,
28}
29
30impl SlashCommandCompletionProvider {
31 pub fn new(
32 slash_commands: Arc<SlashCommandWorkingSet>,
33 editor: Option<WeakView<ContextEditor>>,
34 workspace: Option<WeakView<Workspace>>,
35 ) -> Self {
36 Self {
37 cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
38 slash_commands,
39 editor,
40 workspace,
41 }
42 }
43
44 fn complete_command_name(
45 &self,
46 command_name: &str,
47 command_range: Range<Anchor>,
48 name_range: Range<Anchor>,
49 cx: &mut WindowContext,
50 ) -> Task<Result<Vec<project::Completion>>> {
51 let slash_commands = self.slash_commands.clone();
52 let candidates = slash_commands
53 .command_names(cx)
54 .into_iter()
55 .enumerate()
56 .map(|(ix, def)| StringMatchCandidate::new(ix, &def))
57 .collect::<Vec<_>>();
58 let command_name = command_name.to_string();
59 let editor = self.editor.clone();
60 let workspace = self.workspace.clone();
61 cx.spawn(|mut cx| async move {
62 let matches = match_strings(
63 &candidates,
64 &command_name,
65 true,
66 usize::MAX,
67 &Default::default(),
68 cx.background_executor().clone(),
69 )
70 .await;
71
72 cx.update(|cx| {
73 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, cx: &mut WindowContext| {
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 cx,
107 );
108 })
109 .ok();
110 false
111 } else {
112 requires_argument || accepts_arguments
113 }
114 },
115 ) as Arc<_>
116 });
117 Some(project::Completion {
118 old_range: name_range.clone(),
119 documentation: Some(Documentation::SingleLine(command.description())),
120 new_text,
121 label: command.label(cx),
122 server_id: LanguageServerId(0),
123 lsp_completion: Default::default(),
124 confirm,
125 resolved: true,
126 })
127 })
128 .collect()
129 })
130 })
131 }
132
133 fn complete_command_argument(
134 &self,
135 command_name: &str,
136 arguments: &[String],
137 command_range: Range<Anchor>,
138 argument_range: Range<Anchor>,
139 last_argument_range: Range<Anchor>,
140 cx: &mut WindowContext,
141 ) -> Task<Result<Vec<project::Completion>>> {
142 let new_cancel_flag = Arc::new(AtomicBool::new(false));
143 let mut flag = self.cancel_flag.lock();
144 flag.store(true, SeqCst);
145 *flag = new_cancel_flag.clone();
146 if let Some(command) = self.slash_commands.command(command_name, cx) {
147 let completions = command.complete_argument(
148 arguments,
149 new_cancel_flag.clone(),
150 self.workspace.clone(),
151 cx,
152 );
153 let command_name: Arc<str> = command_name.into();
154 let editor = self.editor.clone();
155 let workspace = self.workspace.clone();
156 let arguments = arguments.to_vec();
157 cx.background_executor().spawn(async move {
158 Ok(completions
159 .await?
160 .into_iter()
161 .map(|new_argument| {
162 let confirm =
163 editor
164 .clone()
165 .zip(workspace.clone())
166 .map(|(editor, workspace)| {
167 Arc::new({
168 let mut completed_arguments = arguments.clone();
169 if new_argument.replace_previous_arguments {
170 completed_arguments.clear();
171 } else {
172 completed_arguments.pop();
173 }
174 completed_arguments.push(new_argument.new_text.clone());
175
176 let command_range = command_range.clone();
177 let command_name = command_name.clone();
178 move |intent: CompletionIntent, cx: &mut WindowContext| {
179 if new_argument.after_completion.run()
180 || intent.is_complete()
181 {
182 editor
183 .update(cx, |editor, cx| {
184 editor.run_command(
185 command_range.clone(),
186 &command_name,
187 &completed_arguments,
188 true,
189 workspace.clone(),
190 cx,
191 );
192 })
193 .ok();
194 false
195 } else {
196 !new_argument.after_completion.run()
197 }
198 }
199 }) as Arc<_>
200 });
201
202 let mut new_text = new_argument.new_text.clone();
203 if new_argument.after_completion == AfterCompletion::Continue {
204 new_text.push(' ');
205 }
206
207 project::Completion {
208 old_range: if new_argument.replace_previous_arguments {
209 argument_range.clone()
210 } else {
211 last_argument_range.clone()
212 },
213 label: new_argument.label,
214 new_text,
215 documentation: None,
216 server_id: LanguageServerId(0),
217 lsp_completion: Default::default(),
218 confirm,
219 resolved: true,
220 }
221 })
222 .collect())
223 })
224 } else {
225 Task::ready(Ok(Vec::new()))
226 }
227 }
228}
229
230impl CompletionProvider for SlashCommandCompletionProvider {
231 fn completions(
232 &self,
233 buffer: &Model<Buffer>,
234 buffer_position: Anchor,
235 _: editor::CompletionContext,
236 cx: &mut ViewContext<Editor>,
237 ) -> Task<Result<Vec<project::Completion>>> {
238 let Some((name, arguments, command_range, last_argument_range)) =
239 buffer.update(cx, |buffer, _cx| {
240 let position = buffer_position.to_point(buffer);
241 let line_start = Point::new(position.row, 0);
242 let mut lines = buffer.text_for_range(line_start..position).lines();
243 let line = lines.next()?;
244 let call = SlashCommandLine::parse(line)?;
245
246 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
247 let command_range_end = Point::new(
248 position.row,
249 call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
250 );
251 let command_range = buffer.anchor_after(command_range_start)
252 ..buffer.anchor_after(command_range_end);
253
254 let name = line[call.name.clone()].to_string();
255 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
256 {
257 let last_arg_start =
258 buffer.anchor_after(Point::new(position.row, argument.start as u32));
259 let first_arg_start = call.arguments.first().expect("we have the last element");
260 let first_arg_start =
261 buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
262 let arguments = call
263 .arguments
264 .iter()
265 .filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
266 .collect::<Vec<_>>();
267 let argument_range = first_arg_start..buffer_position;
268 (
269 Some((arguments, argument_range)),
270 last_arg_start..buffer_position,
271 )
272 } else {
273 let start =
274 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
275 (None, start..buffer_position)
276 };
277
278 Some((name, arguments, command_range, last_argument_range))
279 })
280 else {
281 return Task::ready(Ok(Vec::new()));
282 };
283
284 if let Some((arguments, argument_range)) = arguments {
285 self.complete_command_argument(
286 &name,
287 &arguments,
288 command_range,
289 argument_range,
290 last_argument_range,
291 cx,
292 )
293 } else {
294 self.complete_command_name(&name, command_range, last_argument_range, cx)
295 }
296 }
297
298 fn resolve_completions(
299 &self,
300 _: Model<Buffer>,
301 _: Vec<usize>,
302 _: Rc<RefCell<Box<[project::Completion]>>>,
303 _: &mut ViewContext<Editor>,
304 ) -> Task<Result<bool>> {
305 Task::ready(Ok(true))
306 }
307
308 fn is_completion_trigger(
309 &self,
310 buffer: &Model<Buffer>,
311 position: language::Anchor,
312 _text: &str,
313 _trigger_in_words: bool,
314 cx: &mut ViewContext<Editor>,
315 ) -> bool {
316 let buffer = buffer.read(cx);
317 let position = position.to_point(buffer);
318 let line_start = Point::new(position.row, 0);
319 let mut lines = buffer.text_for_range(line_start..position).lines();
320 if let Some(line) = lines.next() {
321 SlashCommandLine::parse(line).is_some()
322 } else {
323 false
324 }
325 }
326
327 fn sort_completions(&self) -> bool {
328 false
329 }
330}