Skip to main content

steel_core/command/execution/selector/
suggestions.rs

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