Skip to main content

steel_core/command/execution/selector/
suggestions.rs

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