Skip to main content

steel_core/command/execution/
suggestions.rs

1//! Suggestion helpers shared by command arguments
2
3use steel_utils::Identifier;
4
5use crate::command::brigadier::{
6    ArgumentSuggestionContext, CommandArgumentParser, SuggestionProvider, SuggestionsBuilder,
7};
8
9/// Characters separating the segments [`matches_substring`] searches
10const MATCH_SPLITTER: [char; 3] = ['.', '_', '/'];
11
12/// Returns whether `input` starts with `pattern` at the beginning of the input
13/// or of any segment separated by a [`MATCH_SPLITTER`] character.
14pub(in crate::command::execution) fn matches_substring(pattern: &str, input: &str) -> bool {
15    if input.starts_with(pattern) {
16        return true;
17    }
18    input.char_indices().any(|(index, character)| {
19        MATCH_SPLITTER.contains(&character)
20            && input[index + character.len_utf8()..].starts_with(pattern)
21    })
22}
23
24/// [`matches_substring`] on lowercase inputs, as vanilla's
25/// `SharedSuggestionProvider.suggest` does before filtering.
26pub(in crate::command::execution) fn matches_substring_ignore_case(
27    pattern: &str,
28    input: &str,
29) -> bool {
30    matches_substring(&pattern.to_lowercase(), &input.to_lowercase())
31}
32
33/// Matches `pattern` against a whole identifier or, when it carries no
34/// namespace, against its namespace and its path separately.
35pub(in crate::command::execution) fn identifier_matches(
36    pattern: &str,
37    identifier: &Identifier,
38) -> bool {
39    if pattern.contains(':') {
40        matches_substring(pattern, &identifier.to_string())
41    } else {
42        matches_substring(pattern, identifier.namespace.as_ref())
43            || matches_substring(pattern, identifier.path.as_ref())
44    }
45}
46
47/// Suggests the identifiers matching the remaining input.
48pub(in crate::command::execution) fn suggest_resources<'a>(
49    resources: impl Iterator<Item = &'a Identifier>,
50    builder: &mut SuggestionsBuilder<'_>,
51) {
52    let contents = builder.remaining_lowercase();
53    let suggestions = resources
54        .filter(|resource| identifier_matches(contents, resource))
55        .map(Identifier::to_string)
56        .collect::<Vec<_>>();
57    for suggestion in suggestions {
58        builder.suggest(suggestion);
59    }
60}
61
62/// Suggests every value whose name matches the remaining input, keeping the
63/// values' own casing.
64pub(crate) fn suggest_list<T: AsRef<str>>(
65    builder: &mut SuggestionsBuilder<'_>,
66    values: impl IntoIterator<Item = T>,
67) {
68    let lower_prefix = builder.remaining_lowercase().to_owned();
69    for value in values {
70        let value = value.as_ref();
71        if matches_substring(&lower_prefix, &value.to_lowercase()) {
72            builder.suggest(value);
73        }
74    }
75}
76
77/// An implementation of [`SuggestionProvider`] that suggests a constant array of suggestions.
78pub(crate) struct FixedSuggestionProvider {
79    suggestions: &'static [&'static str],
80}
81
82impl FixedSuggestionProvider {
83    pub const fn new(suggestions: &'static [&'static str]) -> Self {
84        Self { suggestions }
85    }
86}
87
88impl<S, A: CommandArgumentParser<S>> SuggestionProvider<S, A> for FixedSuggestionProvider {
89    fn list_suggestions(
90        &self,
91        _context: &ArgumentSuggestionContext<'_, S, A::Value>,
92        builder: &mut SuggestionsBuilder<'_>,
93    ) {
94        suggest_list(builder, self.suggestions.iter().copied());
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::command::brigadier::Suggestion;
102
103    #[test]
104    fn suggest_list_matches_segment_starts_case_insensitively() {
105        let Ok(mut builder) = SuggestionsBuilder::new("adv SLA", 4) else {
106            panic!("suggestion start should be valid");
107        };
108        suggest_list(
109            &mut builder,
110            ["Slab", "stone_slab", "Stone", "cobble_slab", "Restone"],
111        );
112
113        let Ok(suggestions) = builder.build() else {
114            panic!("builder ranges should remain valid");
115        };
116        let texts = suggestions
117            .list()
118            .iter()
119            .map(Suggestion::text)
120            .collect::<Vec<_>>();
121
122        assert_eq!(texts, ["cobble_slab", "Slab", "stone_slab"]);
123    }
124}