Skip to main content

steel_core/command/execution/selector/
suggestions.rs

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}