steel_core/command/execution/
suggestions.rs1use steel_utils::Identifier;
4
5use crate::command::brigadier::{
6 ArgumentSuggestionContext, CommandArgumentParser, SuggestionProvider, SuggestionsBuilder,
7};
8
9const MATCH_SPLITTER: [char; 3] = ['.', '_', '/'];
11
12pub(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
24pub(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
33pub(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
47pub(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
62pub(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
77pub(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}