1use crate::assistant_panel::ContextEditor;
2use anyhow::Result;
3pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
4use editor::{CompletionProvider, Editor};
5use fuzzy::{match_strings, StringMatchCandidate};
6use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
7use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
8use parking_lot::{Mutex, RwLock};
9use project::CompletionIntent;
10use rope::Point;
11use std::{
12 ops::Range,
13 sync::{
14 atomic::{AtomicBool, Ordering::SeqCst},
15 Arc,
16 },
17};
18use ui::ActiveTheme;
19use workspace::Workspace;
20
21pub mod default_command;
22pub mod diagnostics_command;
23pub mod docs_command;
24pub mod fetch_command;
25pub mod file_command;
26pub mod now_command;
27pub mod project_command;
28pub mod prompt_command;
29pub mod search_command;
30pub mod symbols_command;
31pub mod tabs_command;
32pub mod terminal_command;
33pub mod workflow_command;
34
35pub(crate) struct SlashCommandCompletionProvider {
36 cancel_flag: Mutex<Arc<AtomicBool>>,
37 editor: Option<WeakView<ContextEditor>>,
38 workspace: Option<WeakView<Workspace>>,
39}
40
41pub(crate) struct SlashCommandLine {
42 /// The range within the line containing the command name.
43 pub name: Range<usize>,
44 /// The range within the line containing the command argument.
45 pub argument: Option<Range<usize>>,
46}
47
48impl SlashCommandCompletionProvider {
49 pub fn new(
50 editor: Option<WeakView<ContextEditor>>,
51 workspace: Option<WeakView<Workspace>>,
52 ) -> Self {
53 Self {
54 cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
55 editor,
56 workspace,
57 }
58 }
59
60 fn complete_command_name(
61 &self,
62 command_name: &str,
63 command_range: Range<Anchor>,
64 name_range: Range<Anchor>,
65 cx: &mut WindowContext,
66 ) -> Task<Result<Vec<project::Completion>>> {
67 let commands = SlashCommandRegistry::global(cx);
68 let candidates = commands
69 .command_names()
70 .into_iter()
71 .enumerate()
72 .map(|(ix, def)| StringMatchCandidate {
73 id: ix,
74 string: def.to_string(),
75 char_bag: def.as_ref().into(),
76 })
77 .collect::<Vec<_>>();
78 let command_name = command_name.to_string();
79 let editor = self.editor.clone();
80 let workspace = self.workspace.clone();
81 cx.spawn(|mut cx| async move {
82 let matches = match_strings(
83 &candidates,
84 &command_name,
85 true,
86 usize::MAX,
87 &Default::default(),
88 cx.background_executor().clone(),
89 )
90 .await;
91
92 cx.update(|cx| {
93 matches
94 .into_iter()
95 .filter_map(|mat| {
96 let command = commands.command(&mat.string)?;
97 let mut new_text = mat.string.clone();
98 let requires_argument = command.requires_argument();
99 if requires_argument {
100 new_text.push(' ');
101 }
102
103 let confirm = editor.clone().zip(workspace.clone()).and_then(
104 |(editor, workspace)| {
105 (!requires_argument).then(|| {
106 let command_name = mat.string.clone();
107 let command_range = command_range.clone();
108 let editor = editor.clone();
109 let workspace = workspace.clone();
110 Arc::new(
111 move |intent: CompletionIntent, cx: &mut WindowContext| {
112 if intent.is_complete() {
113 editor
114 .update(cx, |editor, cx| {
115 editor.run_command(
116 command_range.clone(),
117 &command_name,
118 None,
119 true,
120 workspace.clone(),
121 cx,
122 );
123 })
124 .ok();
125 }
126 },
127 ) as Arc<_>
128 })
129 },
130 );
131 Some(project::Completion {
132 old_range: name_range.clone(),
133 documentation: Some(Documentation::SingleLine(command.description())),
134 new_text,
135 label: command.label(cx),
136 server_id: LanguageServerId(0),
137 lsp_completion: Default::default(),
138 show_new_completions_on_confirm: requires_argument,
139 confirm,
140 })
141 })
142 .collect()
143 })
144 })
145 }
146
147 fn complete_command_argument(
148 &self,
149 command_name: &str,
150 argument: String,
151 command_range: Range<Anchor>,
152 argument_range: Range<Anchor>,
153 cx: &mut WindowContext,
154 ) -> Task<Result<Vec<project::Completion>>> {
155 let new_cancel_flag = Arc::new(AtomicBool::new(false));
156 let mut flag = self.cancel_flag.lock();
157 flag.store(true, SeqCst);
158 *flag = new_cancel_flag.clone();
159 let commands = SlashCommandRegistry::global(cx);
160 if let Some(command) = commands.command(command_name) {
161 let completions = command.complete_argument(
162 argument,
163 new_cancel_flag.clone(),
164 self.workspace.clone(),
165 cx,
166 );
167 let command_name: Arc<str> = command_name.into();
168 let editor = self.editor.clone();
169 let workspace = self.workspace.clone();
170 cx.background_executor().spawn(async move {
171 Ok(completions
172 .await?
173 .into_iter()
174 .map(|command_argument| {
175 let confirm = if command_argument.run_command {
176 editor
177 .clone()
178 .zip(workspace.clone())
179 .map(|(editor, workspace)| {
180 Arc::new({
181 let command_range = command_range.clone();
182 let command_name = command_name.clone();
183 let command_argument = command_argument.new_text.clone();
184 move |intent: CompletionIntent, cx: &mut WindowContext| {
185 if intent.is_complete() {
186 editor
187 .update(cx, |editor, cx| {
188 editor.run_command(
189 command_range.clone(),
190 &command_name,
191 Some(&command_argument),
192 true,
193 workspace.clone(),
194 cx,
195 );
196 })
197 .ok();
198 }
199 }
200 }) as Arc<_>
201 })
202 } else {
203 None
204 };
205
206 let mut new_text = command_argument.new_text.clone();
207 if !command_argument.run_command {
208 new_text.push(' ');
209 }
210
211 project::Completion {
212 old_range: argument_range.clone(),
213 label: command_argument.label,
214 new_text,
215 documentation: None,
216 server_id: LanguageServerId(0),
217 lsp_completion: Default::default(),
218 show_new_completions_on_confirm: !command_argument.run_command,
219 confirm,
220 }
221 })
222 .collect())
223 })
224 } else {
225 cx.background_executor()
226 .spawn(async move { Ok(Vec::new()) })
227 }
228 }
229}
230
231impl CompletionProvider for SlashCommandCompletionProvider {
232 fn completions(
233 &self,
234 buffer: &Model<Buffer>,
235 buffer_position: Anchor,
236 _: editor::CompletionContext,
237 cx: &mut ViewContext<Editor>,
238 ) -> Task<Result<Vec<project::Completion>>> {
239 let Some((name, argument, command_range, argument_range)) =
240 buffer.update(cx, |buffer, _cx| {
241 let position = buffer_position.to_point(buffer);
242 let line_start = Point::new(position.row, 0);
243 let mut lines = buffer.text_for_range(line_start..position).lines();
244 let line = lines.next()?;
245 let call = SlashCommandLine::parse(line)?;
246
247 let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
248 let command_range_end = Point::new(
249 position.row,
250 call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
251 );
252 let command_range = buffer.anchor_after(command_range_start)
253 ..buffer.anchor_after(command_range_end);
254
255 let name = line[call.name.clone()].to_string();
256
257 Some(if let Some(argument) = call.argument {
258 let start =
259 buffer.anchor_after(Point::new(position.row, argument.start as u32));
260 let argument = line[argument.clone()].to_string();
261 (name, Some(argument), command_range, start..buffer_position)
262 } else {
263 let start =
264 buffer.anchor_after(Point::new(position.row, call.name.start as u32));
265 (name, None, command_range, start..buffer_position)
266 })
267 })
268 else {
269 return Task::ready(Ok(Vec::new()));
270 };
271
272 if let Some(argument) = argument {
273 self.complete_command_argument(&name, argument, command_range, argument_range, cx)
274 } else {
275 self.complete_command_name(&name, command_range, argument_range, cx)
276 }
277 }
278
279 fn resolve_completions(
280 &self,
281 _: Model<Buffer>,
282 _: Vec<usize>,
283 _: Arc<RwLock<Box<[project::Completion]>>>,
284 _: &mut ViewContext<Editor>,
285 ) -> Task<Result<bool>> {
286 Task::ready(Ok(true))
287 }
288
289 fn apply_additional_edits_for_completion(
290 &self,
291 _: Model<Buffer>,
292 _: project::Completion,
293 _: bool,
294 _: &mut ViewContext<Editor>,
295 ) -> Task<Result<Option<language::Transaction>>> {
296 Task::ready(Ok(None))
297 }
298
299 fn is_completion_trigger(
300 &self,
301 buffer: &Model<Buffer>,
302 position: language::Anchor,
303 _text: &str,
304 _trigger_in_words: bool,
305 cx: &mut ViewContext<Editor>,
306 ) -> bool {
307 let buffer = buffer.read(cx);
308 let position = position.to_point(buffer);
309 let line_start = Point::new(position.row, 0);
310 let mut lines = buffer.text_for_range(line_start..position).lines();
311 if let Some(line) = lines.next() {
312 SlashCommandLine::parse(line).is_some()
313 } else {
314 false
315 }
316 }
317}
318
319impl SlashCommandLine {
320 pub(crate) fn parse(line: &str) -> Option<Self> {
321 let mut call: Option<Self> = None;
322 let mut ix = 0;
323 for c in line.chars() {
324 let next_ix = ix + c.len_utf8();
325 if let Some(call) = &mut call {
326 // The command arguments start at the first non-whitespace character
327 // after the command name, and continue until the end of the line.
328 if let Some(argument) = &mut call.argument {
329 if (*argument).is_empty() && c.is_whitespace() {
330 argument.start = next_ix;
331 }
332 argument.end = next_ix;
333 }
334 // The command name ends at the first whitespace character.
335 else if !call.name.is_empty() {
336 if c.is_whitespace() {
337 call.argument = Some(next_ix..next_ix);
338 } else {
339 call.name.end = next_ix;
340 }
341 }
342 // The command name must begin with a letter.
343 else if c.is_alphabetic() {
344 call.name.end = next_ix;
345 } else {
346 return None;
347 }
348 }
349 // Commands start with a slash.
350 else if c == '/' {
351 call = Some(SlashCommandLine {
352 name: next_ix..next_ix,
353 argument: None,
354 });
355 }
356 // The line can't contain anything before the slash except for whitespace.
357 else if !c.is_whitespace() {
358 return None;
359 }
360 ix = next_ix;
361 }
362 call
363 }
364}
365
366pub fn create_label_for_command(
367 command_name: &str,
368 arguments: &[&str],
369 cx: &AppContext,
370) -> CodeLabel {
371 let mut label = CodeLabel::default();
372 label.push_str(command_name, None);
373 label.push_str(" ", None);
374 label.push_str(
375 &arguments.join(" "),
376 cx.theme().syntax().highlight_id("comment").map(HighlightId),
377 );
378 label.filter_range = 0..command_name.len();
379 label
380}