1use super::*;
2
3fn selector_suggestions(allow_selectors: bool) -> Vec<&'static str> {
4 if !allow_selectors {
5 return Vec::new();
6 }
7 vec!["@a", "@e", "@p", "@r", "@s", "@n"]
8}
9
10struct SelectorSuggestionData {
11 allow_selectors: bool,
12 allow_advanced: bool,
13 player_names: Vec<String>,
14 team_names: Vec<String>,
15}
16
17pub(crate) fn suggest_entity_selector<S>(
18 builder: &mut SuggestionsBuilder<'_>,
19 source: &S,
20 single: bool,
21 players_only: bool,
22) where
23 S: CommandArgumentSource + ?Sized,
24{
25 let data = SelectorSuggestionData {
26 allow_selectors: allow_selectors(source),
27 allow_advanced: allow_advanced_selectors(source),
28 player_names: source.selector_player_names(),
29 team_names: source.selector_team_names(),
30 };
31 for suggestion in
32 selector_argument_suggestions(builder.remaining(), players_only, single, &data)
33 {
34 builder.suggest(suggestion);
35 }
36}
37
38fn selector_argument_suggestions(
39 prefix: &str,
40 players_only: bool,
41 single: bool,
42 data: &SelectorSuggestionData,
43) -> Vec<String> {
44 if !prefix.starts_with('@') {
45 return selector_root_suggestions(prefix, players_only, single, data);
46 }
47
48 let mut chars = prefix.chars();
49 if chars.next() != Some('@') {
50 return Vec::new();
51 }
52 let Some(selector_type) = chars.next() else {
53 return selector_root_suggestions(prefix, players_only, single, data);
54 };
55 if !selector_type_allowed_for_suggestions(selector_type) {
56 return selector_root_suggestions(prefix, players_only, single, data);
57 }
58 if chars.next().is_some_and(|ch| ch != '[') {
59 return selector_root_suggestions(prefix, players_only, single, data);
60 }
61
62 if let Some(option_start) = prefix.find('[') {
63 if !data.allow_advanced {
64 return Vec::new();
65 }
66 return selector_option_suggestions(prefix, selector_type, option_start, data);
67 }
68
69 if !data.allow_advanced {
70 return selector_root_suggestions(prefix, players_only, single, data);
71 }
72 let open_options = format!("@{selector_type}[");
73 if open_options.starts_with(prefix) {
74 vec![open_options]
75 } else {
76 selector_root_suggestions(prefix, players_only, single, data)
77 }
78}
79
80fn selector_root_suggestions(
81 prefix: &str,
82 _players_only: bool,
83 _single: bool,
84 data: &SelectorSuggestionData,
85) -> Vec<String> {
86 let mut suggestions = selector_suggestions(data.allow_selectors)
87 .into_iter()
88 .filter(|selector| selector.starts_with(prefix))
89 .map(str::to_owned)
90 .collect::<Vec<_>>();
91 suggestions.extend(
92 data.player_names
93 .iter()
94 .filter(|name| matches_generic_suggestion(prefix, name))
95 .cloned(),
96 );
97 suggestions
98}
99
100const fn selector_type_allowed_for_suggestions(selector_type: char) -> bool {
101 matches!(selector_type, 'a' | 'e' | 'n' | 'p' | 'r' | 's')
102}
103
104fn selector_option_suggestions(
105 prefix: &str,
106 selector_type: char,
107 option_start: usize,
108 data: &SelectorSuggestionData,
109) -> Vec<String> {
110 if selector_options_have_top_level_close(&prefix[option_start + 1..]) {
111 return Vec::new();
112 }
113
114 let option_prefix = &prefix[..=option_start];
115 let inside = &prefix[option_start + 1..];
116 let (completed_entries, current_entry) = split_current_selector_option_entry(inside);
117 let expression_prefix = format!("{option_prefix}{completed_entries}");
118 if let Some((key, value_prefix)) = current_entry.split_once('=') {
119 let value_expression_prefix = format!("{expression_prefix}{key}=");
120 let mut suggestions = selector_option_value_suggestions(
121 &value_expression_prefix,
122 key.trim(),
123 value_prefix,
124 completed_entries,
125 data,
126 );
127 suggestions.retain(|suggestion| suggestion != prefix);
128 if selector_option_entry_is_complete(selector_type, inside) {
129 suggestions.extend(selector_option_delimiter_suggestions(prefix));
130 }
131 return suggestions;
132 }
133
134 let used_set_once_options = completed_set_once_selector_options(completed_entries);
135 let mut suggestions = Vec::new();
136 if completed_entries.is_empty() && current_entry.trim().is_empty() {
137 suggestions.push(format!("{option_prefix}]"));
138 }
139 suggestions.extend(
140 SELECTOR_OPTION_KEYS
141 .iter()
142 .copied()
143 .filter(|key| selector_option_supported_for_suggestions(key))
144 .filter(|key| selector_option_available_for_type(key, selector_type))
145 .filter(|key| !used_set_once_options.iter().any(|used| used == key))
146 .filter(|key| selector_option_available_for_completed_entries(key, completed_entries))
147 .filter(|key| matches_generic_suggestion(current_entry.trim_start(), key))
148 .map(|key| format!("{expression_prefix}{key}=")),
149 );
150 suggestions
151}
152
153fn selector_option_entry_is_complete(selector_type: char, inside: &str) -> bool {
154 parse_selector_plan_with_permissions(&format!("@{selector_type}[{inside}]"), true, true).is_ok()
155}
156
157fn selector_option_delimiter_suggestions(prefix: &str) -> Vec<String> {
158 [',', ']']
159 .iter()
160 .map(|delimiter| format!("{prefix}{delimiter}"))
161 .collect()
162}
163
164fn selector_option_supported_for_suggestions(key: &str) -> bool {
165 !UNSUPPORTED_SELECTOR_OPTION_KEYS.contains(&key)
166}
167
168fn selector_options_have_top_level_close(input: &str) -> bool {
169 let mut state = SelectorSuggestionSplitState::default();
170 for (_, ch) in input.char_indices() {
171 if state.accepts_top_level_close(ch) {
172 return true;
173 }
174 }
175 false
176}
177
178fn split_current_selector_option_entry(input: &str) -> (&str, &str) {
179 let mut state = SelectorSuggestionSplitState::default();
180 let mut separator = None;
181 for (index, ch) in input.char_indices() {
182 if state.accepts_top_level_separator(ch) {
183 separator = Some(index);
184 }
185 }
186
187 separator.map_or(("", input), |index| (&input[..=index], &input[index + 1..]))
188}
189
190fn selector_option_entries(input: &str) -> Vec<&str> {
191 let mut entries = Vec::new();
192 let mut state = SelectorSuggestionSplitState::default();
193 let mut entry_start = 0;
194 for (index, ch) in input.char_indices() {
195 if state.accepts_top_level_separator(ch) {
196 let entry = input[entry_start..index].trim();
197 if !entry.is_empty() {
198 entries.push(entry);
199 }
200 entry_start = index + ch.len_utf8();
201 }
202 }
203
204 let entry = input[entry_start..].trim();
205 if !entry.is_empty() {
206 entries.push(entry);
207 }
208 entries
209}
210
211#[derive(Default)]
212struct SelectorSuggestionSplitState {
213 depth: usize,
214 quote: Option<char>,
215 escaping: bool,
216}
217
218impl SelectorSuggestionSplitState {
219 const fn accepts_top_level_separator(&mut self, ch: char) -> bool {
220 self.accepts_top_level_char(ch, ',')
221 }
222
223 const fn accepts_top_level_close(&mut self, ch: char) -> bool {
224 self.accepts_top_level_char(ch, ']')
225 }
226
227 const fn accepts_top_level_char(&mut self, ch: char, target: char) -> bool {
228 if let Some(quote) = self.quote {
229 if self.escaping {
230 self.escaping = false;
231 return false;
232 }
233 if ch == '\\' {
234 self.escaping = true;
235 return false;
236 }
237 if ch == quote {
238 self.quote = None;
239 }
240 return false;
241 }
242
243 match ch {
244 '"' | '\'' => self.quote = Some(ch),
245 '{' | '[' | '(' => self.depth = self.depth.saturating_add(1),
246 ']' if self.depth == 0 => return target == ']',
247 '}' | ')' | ']' => self.depth = self.depth.saturating_sub(1),
248 _ if ch == target && self.depth == 0 => return true,
249 _ => {}
250 }
251 false
252 }
253}
254
255fn completed_set_once_selector_options(completed_entries: &str) -> Vec<&str> {
256 selector_option_entries(completed_entries)
257 .into_iter()
258 .filter_map(|entry| entry.split_once('=').map(|(key, _)| key.trim()))
259 .filter(|key| SET_ONCE_SELECTOR_OPTIONS.contains(key))
260 .collect()
261}
262
263fn selector_option_available_for_type(key: &str, selector_type: char) -> bool {
264 !matches!((key, selector_type), ("limit" | "sort", 's'))
265}
266
267fn selector_option_available_for_completed_entries(key: &str, completed_entries: &str) -> bool {
268 match key {
269 "name" | "gamemode" | "team" => completed_invertable_option_state(completed_entries, key)
270 .suggestion_mode()
271 .allows_any(),
272 "type" => completed_entity_type_suggestion_state(completed_entries)
273 .mode
274 .allows_any(),
275 _ => true,
276 }
277}
278
279fn selector_option_value_suggestions(
280 expression_prefix: &str,
281 key: &str,
282 value_prefix: &str,
283 completed_entries: &str,
284 data: &SelectorSuggestionData,
285) -> Vec<String> {
286 match key {
287 "sort" => prefixed_values(
288 expression_prefix,
289 value_prefix,
290 [SORT_NEAREST, SORT_FURTHEST, SORT_RANDOM, SORT_ARBITRARY],
291 ),
292 "gamemode" => invertible_prefixed_values(
293 expression_prefix,
294 value_prefix,
295 GAME_MODE_SUGGESTIONS,
296 completed_invertable_option_state(completed_entries, key).suggestion_mode(),
297 ),
298 "type" => entity_type_suggestions(
299 expression_prefix,
300 value_prefix,
301 &completed_entity_type_suggestion_state(completed_entries),
302 ),
303 "team" => team_suggestions(
304 expression_prefix,
305 value_prefix,
306 data,
307 completed_invertable_option_state(completed_entries, key).suggestion_mode(),
308 ),
309 _ => Vec::new(),
310 }
311}
312
313fn completed_invertable_option_state(completed_entries: &str, key: &str) -> InvertableOptionState {
314 let mut state = InvertableOptionState::default();
315 for value in completed_option_values(completed_entries, key) {
316 let _ = state.parse_element(value.trim_start().starts_with('!'), key);
317 }
318 state
319}
320
321fn completed_option_values<'a>(
322 completed_entries: &'a str,
323 key: &'a str,
324) -> impl Iterator<Item = &'a str> {
325 selector_option_entries(completed_entries)
326 .into_iter()
327 .filter_map(|entry| entry.split_once('='))
328 .filter(move |(entry_key, _)| entry_key.trim() == key)
329 .map(|(_, value)| value.trim())
330 .filter(|value| !value.is_empty())
331}
332
333fn prefixed_values<const N: usize>(
334 expression_prefix: &str,
335 value_prefix: &str,
336 values: [&'static str; N],
337) -> Vec<String> {
338 values
339 .into_iter()
340 .filter(|value| value.starts_with(value_prefix))
341 .map(|value| format!("{expression_prefix}{value}"))
342 .collect()
343}
344
345fn invertible_prefixed_values(
346 expression_prefix: &str,
347 value_prefix: &str,
348 values: &[&'static str],
349 mode: InvertableSuggestionMode,
350) -> Vec<String> {
351 let mut suggestions = Vec::new();
352 for value in values {
353 if mode.allows_positive() {
354 push_prefixed_value(&mut suggestions, expression_prefix, value_prefix, value);
355 }
356 if mode.allows_negative() {
357 push_prefixed_value(
358 &mut suggestions,
359 expression_prefix,
360 value_prefix,
361 &format!("!{value}"),
362 );
363 }
364 }
365 suggestions
366}
367
368fn push_prefixed_value(
369 suggestions: &mut Vec<String>,
370 expression_prefix: &str,
371 value_prefix: &str,
372 value: &str,
373) {
374 if matches_generic_suggestion(value_prefix, value) {
375 suggestions.push(format!("{expression_prefix}{value}"));
376 }
377}
378
379#[derive(Clone, Debug)]
380struct EntityTypeSuggestionState {
381 mode: InvertableSuggestionMode,
382 tags_seen: Vec<Identifier>,
383}
384
385fn completed_entity_type_suggestion_state(completed_entries: &str) -> EntityTypeSuggestionState {
386 let mut state = InvertableOptionState::default();
387 let mut tags_seen = Vec::new();
388 for value in completed_option_values(completed_entries, "type") {
389 let value = value.trim_start();
390 if let Some(tag) = value.strip_prefix("!#").or_else(|| value.strip_prefix('#')) {
391 if let Some(tag) = parse_resource_identifier_value(tag)
392 && !tags_seen.iter().any(|seen| seen == &tag)
393 {
394 tags_seen.push(tag);
395 }
396 state.negative_seen = true;
397 } else {
398 let _ = state.parse_element(value.starts_with('!'), "type");
399 }
400 }
401
402 EntityTypeSuggestionState {
403 mode: state.suggestion_mode(),
404 tags_seen,
405 }
406}
407
408fn entity_type_suggestions(
409 expression_prefix: &str,
410 value_prefix: &str,
411 state: &EntityTypeSuggestionState,
412) -> Vec<String> {
413 if !state.mode.allows_any() {
414 return Vec::new();
415 }
416
417 let mut suggestions = Vec::new();
418 push_entity_type_tag_suggestions(&mut suggestions, expression_prefix, value_prefix, "", state);
419 push_entity_type_tag_suggestions(
420 &mut suggestions,
421 expression_prefix,
422 value_prefix,
423 "!",
424 state,
425 );
426 if value_prefix.starts_with('#') || value_prefix.starts_with("!#") {
427 return suggestions;
428 }
429
430 if state.mode.allows_positive() {
431 push_entity_type_id_suggestions(&mut suggestions, expression_prefix, value_prefix, "");
432 }
433 if state.mode.allows_negative() {
434 push_entity_type_id_suggestions(&mut suggestions, expression_prefix, value_prefix, "!");
435 }
436
437 suggestions
438}
439
440fn push_entity_type_id_suggestions(
441 suggestions: &mut Vec<String>,
442 expression_prefix: &str,
443 value_prefix: &str,
444 inversion: &str,
445) {
446 let resource_prefix = if inversion.is_empty() {
447 if value_prefix.starts_with('!') || value_prefix.starts_with('#') {
448 return;
449 }
450 value_prefix
451 } else if let Some(prefix) = value_prefix.strip_prefix(inversion) {
452 prefix
453 } else if inversion.starts_with(value_prefix) {
454 ""
455 } else {
456 return;
457 };
458
459 let stripped_prefix = resource_prefix
460 .strip_prefix("minecraft:")
461 .unwrap_or(resource_prefix);
462 suggestions.extend(
463 REGISTRY
464 .entity_types
465 .iter()
466 .map(|(_, entity_type)| entity_type.key.to_string())
467 .filter(|key| {
468 let text = key.strip_prefix("minecraft:").unwrap_or(key);
469 matches_suggestion_substring(stripped_prefix, text)
470 })
471 .map(|key| format!("{expression_prefix}{inversion}{key}")),
472 );
473}
474
475fn push_entity_type_tag_suggestions(
476 suggestions: &mut Vec<String>,
477 expression_prefix: &str,
478 value_prefix: &str,
479 inversion: &str,
480 state: &EntityTypeSuggestionState,
481) {
482 let marker = format!("{inversion}#");
483 if !marker.starts_with(value_prefix) && !value_prefix.starts_with(&marker) {
484 return;
485 }
486
487 let tag_prefix = value_prefix.strip_prefix(&marker).unwrap_or_default();
488 let tag_prefix = tag_prefix.strip_prefix("minecraft:").unwrap_or(tag_prefix);
489 let mut tag_keys = REGISTRY.entity_types.tag_keys().collect::<Vec<_>>();
490 tag_keys.sort_by(|left, right| {
491 left.namespace
492 .cmp(&right.namespace)
493 .then_with(|| left.path.cmp(&right.path))
494 });
495 suggestions.extend(
496 tag_keys
497 .into_iter()
498 .filter(|key| !state.tags_seen.iter().any(|seen| seen == *key))
499 .filter(|key| {
500 if key.namespace == Identifier::VANILLA_NAMESPACE {
501 return matches_suggestion_substring(tag_prefix, &key.path);
502 }
503
504 let text = key.to_string();
505 matches_suggestion_substring(tag_prefix, &text)
506 })
507 .map(|key| format!("{expression_prefix}{marker}{key}")),
508 );
509}
510
511fn team_suggestions(
512 expression_prefix: &str,
513 value_prefix: &str,
514 data: &SelectorSuggestionData,
515 mode: InvertableSuggestionMode,
516) -> Vec<String> {
517 let mut suggestions = Vec::new();
518 for team_name in &data.team_names {
519 if mode.allows_positive() {
520 push_prefixed_value(&mut suggestions, expression_prefix, value_prefix, team_name);
521 }
522 if mode.allows_negative() {
523 push_prefixed_value(
524 &mut suggestions,
525 expression_prefix,
526 value_prefix,
527 &format!("!{team_name}"),
528 );
529 }
530 }
531 suggestions
532}
533
534fn matches_suggestion_substring(pattern: &str, input: &str) -> bool {
535 if input.starts_with(pattern) {
536 return true;
537 }
538 input.char_indices().any(|(index, character)| {
539 matches!(character, '.' | '_' | '/')
540 && input[index + character.len_utf8()..].starts_with(pattern)
541 })
542}
543
544fn matches_generic_suggestion(pattern: &str, input: &str) -> bool {
545 matches_suggestion_substring(&pattern.to_lowercase(), &input.to_lowercase())
546}