1use anyhow::Result;
2use editor::{CompletionProvider, Editor};
3use fuzzy::{match_strings, StringMatchCandidate};
4use gpui::{AppContext, Model, Task, ViewContext};
5use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
6use parking_lot::{Mutex, RwLock};
7use rope::Point;
8use std::{
9 ops::Range,
10 sync::{
11 atomic::{AtomicBool, Ordering::SeqCst},
12 Arc,
13 },
14};
15
16pub use assistant_slash_command::{
17 SlashCommand, SlashCommandCleanup, SlashCommandInvocation, SlashCommandRegistry,
18};
19
20pub mod current_file_command;
21pub mod file_command;
22pub mod prompt_command;
23
24pub(crate) struct SlashCommandCompletionProvider {
25 commands: Arc<SlashCommandRegistry>,
26 cancel_flag: Mutex<Arc<AtomicBool>>,
27}
28
29pub(crate) struct SlashCommandLine {
30 /// The range within the line containing the command name.
31 pub name: Range<usize>,
32 /// The range within the line containing the command argument.
33 pub argument: Option<Range<usize>>,
34}
35
36impl SlashCommandCompletionProvider {
37 pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
38 Self {
39 cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
40 commands,
41 }
42 }
43
44 fn complete_command_name(
45 &self,
46 command_name: &str,
47 range: Range<Anchor>,
48 cx: &mut AppContext,
49 ) -> Task<Result<Vec<project::Completion>>> {
50 let candidates = self
51 .commands
52 .command_names()
53 .into_iter()
54 .enumerate()
55 .map(|(ix, def)| StringMatchCandidate {
56 id: ix,
57 string: def.to_string(),
58 char_bag: def.as_ref().into(),
59 })
60 .collect::<Vec<_>>();
61 let commands = self.commands.clone();
62 let command_name = command_name.to_string();
63 let executor = cx.background_executor().clone();
64 executor.clone().spawn(async move {
65 let matches = match_strings(
66 &candidates,
67 &command_name,
68 true,
69 usize::MAX,
70 &Default::default(),
71 executor,
72 )
73 .await;
74
75 Ok(matches
76 .into_iter()
77 .filter_map(|mat| {
78 let command = commands.command(&mat.string)?;
79 let mut new_text = mat.string.clone();
80 if command.requires_argument() {
81 new_text.push(' ');
82 }
83
84 Some(project::Completion {
85 old_range: range.clone(),
86 documentation: Some(Documentation::SingleLine(command.description())),
87 new_text,
88 label: CodeLabel::plain(mat.string, None),
89 server_id: LanguageServerId(0),
90 lsp_completion: Default::default(),
91 })
92 })
93 .collect())
94 })
95 }
96
97 fn complete_command_argument(
98 &self,
99 command_name: &str,
100 argument: String,
101 range: Range<Anchor>,
102 cx: &mut AppContext,
103 ) -> Task<Result<Vec<project::Completion>>> {
104 let new_cancel_flag = Arc::new(AtomicBool::new(false));
105 let mut flag = self.cancel_flag.lock();
106 flag.store(true, SeqCst);
107 *flag = new_cancel_flag.clone();
108
109 if let Some(command) = self.commands.command(command_name) {
110 let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
111 cx.background_executor().spawn(async move {
112 Ok(completions
113 .await?
114 .into_iter()
115 .map(|arg| project::Completion {
116 old_range: range.clone(),
117 label: CodeLabel::plain(arg.clone(), None),
118 new_text: arg.clone(),
119 documentation: None,
120 server_id: LanguageServerId(0),
121 lsp_completion: Default::default(),
122 })
123 .collect())
124 })
125 } else {
126 cx.background_executor()
127 .spawn(async move { Ok(Vec::new()) })
128 }
129 }
130}
131
132impl CompletionProvider for SlashCommandCompletionProvider {
133 fn completions(
134 &self,
135 buffer: &Model<Buffer>,
136 buffer_position: Anchor,
137 cx: &mut ViewContext<Editor>,
138 ) -> Task<Result<Vec<project::Completion>>> {
139 let task = buffer.update(cx, |buffer, cx| {
140 let position = buffer_position.to_point(buffer);
141 let line_start = Point::new(position.row, 0);
142 let mut lines = buffer.text_for_range(line_start..position).lines();
143 let line = lines.next()?;
144 let call = SlashCommandLine::parse(line)?;
145
146 let name = &line[call.name.clone()];
147 if let Some(argument) = call.argument {
148 let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
149 let argument = line[argument.clone()].to_string();
150 Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
151 } else {
152 let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
153 Some(self.complete_command_name(name, start..buffer_position, cx))
154 }
155 });
156
157 task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
158 }
159
160 fn resolve_completions(
161 &self,
162 _: Model<Buffer>,
163 _: Vec<usize>,
164 _: Arc<RwLock<Box<[project::Completion]>>>,
165 _: &mut ViewContext<Editor>,
166 ) -> Task<Result<bool>> {
167 Task::ready(Ok(true))
168 }
169
170 fn apply_additional_edits_for_completion(
171 &self,
172 _: Model<Buffer>,
173 _: project::Completion,
174 _: bool,
175 _: &mut ViewContext<Editor>,
176 ) -> Task<Result<Option<language::Transaction>>> {
177 Task::ready(Ok(None))
178 }
179
180 fn is_completion_trigger(
181 &self,
182 buffer: &Model<Buffer>,
183 position: language::Anchor,
184 _text: &str,
185 _trigger_in_words: bool,
186 cx: &mut ViewContext<Editor>,
187 ) -> bool {
188 let buffer = buffer.read(cx);
189 let position = position.to_point(buffer);
190 let line_start = Point::new(position.row, 0);
191 let mut lines = buffer.text_for_range(line_start..position).lines();
192 if let Some(line) = lines.next() {
193 SlashCommandLine::parse(line).is_some()
194 } else {
195 false
196 }
197 }
198}
199
200impl SlashCommandLine {
201 pub(crate) fn parse(line: &str) -> Option<Self> {
202 let mut call: Option<Self> = None;
203 let mut ix = 0;
204 for c in line.chars() {
205 let next_ix = ix + c.len_utf8();
206 if let Some(call) = &mut call {
207 // The command arguments start at the first non-whitespace character
208 // after the command name, and continue until the end of the line.
209 if let Some(argument) = &mut call.argument {
210 if (*argument).is_empty() && c.is_whitespace() {
211 argument.start = next_ix;
212 }
213 argument.end = next_ix;
214 }
215 // The command name ends at the first whitespace character.
216 else if !call.name.is_empty() {
217 if c.is_whitespace() {
218 call.argument = Some(next_ix..next_ix);
219 } else {
220 call.name.end = next_ix;
221 }
222 }
223 // The command name must begin with a letter.
224 else if c.is_alphabetic() {
225 call.name.end = next_ix;
226 } else {
227 return None;
228 }
229 }
230 // Commands start with a slash.
231 else if c == '/' {
232 call = Some(SlashCommandLine {
233 name: next_ix..next_ix,
234 argument: None,
235 });
236 }
237 // The line can't contain anything before the slash except for whitespace.
238 else if !c.is_whitespace() {
239 return None;
240 }
241 ix = next_ix;
242 }
243 call
244 }
245}