Skip to main content

steel_core/command/execution/block/
suggestions.rs

1//! Stateful completions for block states and block predicates.
2
3use steel_registry::{
4    REGISTRY, RegistryExt as _, TaggedRegistryExt as _,
5    blocks::{BlockRef, block_state_ext::BlockStateExt as _},
6};
7use steel_utils::Identifier;
8
9use super::super::argument::parse_identifier;
10use crate::command::brigadier::{StringReader, SuggestionsBuilder};
11use crate::command::execution::suggestions::identifier_matches;
12
13pub(in crate::command::execution) fn suggest_blocks(builder: &mut SuggestionsBuilder<'_>) {
14    suggest_blocks_and_tags(builder, true);
15}
16
17pub(in crate::command::execution) fn suggest_block_inputs(builder: &mut SuggestionsBuilder<'_>) {
18    suggest_blocks_and_tags(builder, false);
19}
20
21fn suggest_blocks_and_tags(builder: &mut SuggestionsBuilder<'_>, include_tags: bool) {
22    let input = builder.remaining().to_owned();
23    let Some(syntax_start) = input.find(['[', '{']) else {
24        if let Some(target) = SuggestedBlockTarget::parse(&input, include_tags) {
25            if target.has_properties() {
26                builder.suggest(format!("{input}["));
27            }
28            if target.has_block_entity() {
29                builder.suggest(format!("{input}{{"));
30            }
31        } else {
32            suggest_block_resources(&input, include_tags, builder);
33        }
34        return;
35    };
36
37    let resource = &input[..syntax_start];
38    let Some(target) = SuggestedBlockTarget::parse(resource, include_tags) else {
39        return;
40    };
41    match input.as_bytes()[syntax_start] {
42        b'[' => suggest_block_properties(&input, syntax_start, &target, builder),
43        b'{' => {}
44        _ => unreachable!("syntax search only returns block-state delimiters"),
45    }
46}
47
48fn suggest_block_resources(input: &str, include_tags: bool, builder: &mut SuggestionsBuilder<'_>) {
49    let lowercase = input.to_lowercase();
50    if let Some(prefix) = lowercase.strip_prefix('#') {
51        if include_tags {
52            for tag in REGISTRY
53                .blocks
54                .tag_keys()
55                .filter(|tag| identifier_matches(prefix, tag))
56            {
57                builder.suggest(format!("#{tag}"));
58            }
59        }
60        return;
61    }
62    for block in REGISTRY
63        .blocks
64        .iter()
65        .map(|(_, block)| &block.key)
66        .filter(|key| identifier_matches(&lowercase, key))
67    {
68        builder.suggest(block.to_string());
69    }
70    if include_tags {
71        for tag in REGISTRY
72            .blocks
73            .tag_keys()
74            .filter(|tag| identifier_matches(&lowercase, tag))
75        {
76            builder.suggest(format!("#{tag}"));
77        }
78    }
79}
80
81fn suggest_block_properties(
82    input: &str,
83    syntax_start: usize,
84    target: &SuggestedBlockTarget,
85    builder: &mut SuggestionsBuilder<'_>,
86) {
87    let body = &input[syntax_start + 1..];
88    if let Some(end) = find_unquoted(body, ']') {
89        if body[end + 1..].is_empty() && target.has_block_entity() {
90            builder.suggest(format!("{input}{{"));
91        }
92        return;
93    }
94
95    let current_start = current_property_start(body);
96    let current = &body[current_start..];
97    let leading_whitespace = current.len() - current.trim_start().len();
98    let prefix_end = syntax_start + 1 + current_start + leading_whitespace;
99    let prefix = &input[..prefix_end];
100    let current = &current[leading_whitespace..];
101    let visited = completed_property_names(&body[..current_start]);
102
103    let Some(equals) = find_unquoted(current, '=') else {
104        let key = parsed_command_string(current).unwrap_or_else(|| current.trim().to_owned());
105        if !visited.iter().any(|seen| seen == &key) && target.has_property(&key) {
106            builder.suggest(format!("{input}="));
107        } else if current.trim().len() == current.len() {
108            let lowercase = key.to_lowercase();
109            for property in target.property_names() {
110                if !visited.iter().any(|seen| seen == property) && property.starts_with(&lowercase)
111                {
112                    builder.suggest(format!("{prefix}{property}="));
113                }
114            }
115        }
116        if current_start == 0 && current.trim().is_empty() {
117            builder.suggest(format!("{prefix}]"));
118        }
119        return;
120    };
121
122    let Some(property) = parsed_command_string(&current[..equals]) else {
123        return;
124    };
125    if visited.iter().any(|seen| seen == &property)
126        || (!target.is_tag && !target.has_property(&property))
127    {
128        return;
129    }
130    let value_input = &current[equals + 1..];
131    let value_whitespace = value_input.len() - value_input.trim_start().len();
132    let value_prefix_end = prefix_end + equals + 1 + value_whitespace;
133    let value_prefix = &input[..value_prefix_end];
134    let value_input = &value_input[value_whitespace..];
135    let value = parsed_command_string(value_input).unwrap_or_else(|| value_input.trim().to_owned());
136    let value_is_exact = target.has_property_value(&property, &value);
137
138    if target.is_tag || !value_is_exact {
139        for possible in target.property_value_names(&property) {
140            builder.suggest(format!("{value_prefix}{possible}"));
141        }
142    }
143    if target.is_tag || value_is_exact {
144        let has_more = target
145            .property_names()
146            .into_iter()
147            .any(|name| name != property && !visited.iter().any(|seen| seen == name));
148        if has_more {
149            builder.suggest(format!("{input},"));
150        }
151        builder.suggest(format!("{input}]"));
152    }
153}
154
155struct SuggestedBlockTarget {
156    blocks: Vec<BlockRef>,
157    is_tag: bool,
158}
159
160impl SuggestedBlockTarget {
161    fn parse(input: &str, include_tags: bool) -> Option<Self> {
162        if let Some(tag) = input.strip_prefix('#') {
163            if !include_tags {
164                return None;
165            }
166            let key = parse_suggestion_identifier(tag)?;
167            return Some(Self {
168                blocks: REGISTRY.blocks.get_tag(&key)?,
169                is_tag: true,
170            });
171        }
172        let key = parse_suggestion_identifier(input)?;
173        Some(Self {
174            blocks: vec![REGISTRY.blocks.by_key(&key)?],
175            is_tag: false,
176        })
177    }
178
179    fn property_names(&self) -> Vec<&'static str> {
180        let mut names = Vec::new();
181        for block in &self.blocks {
182            for property in block.properties {
183                let name = property.get_name();
184                if !names.contains(&name) {
185                    names.push(name);
186                }
187            }
188        }
189        names
190    }
191
192    fn property_value_names(&self, name: &str) -> Vec<&str> {
193        let mut values = Vec::new();
194        for block in &self.blocks {
195            let Some(property) = block
196                .properties
197                .iter()
198                .find(|property| property.get_name() == name)
199            else {
200                continue;
201            };
202            for value in property.get_possible_value_names() {
203                if !values.contains(&value) {
204                    values.push(value);
205                }
206            }
207        }
208        values
209    }
210
211    fn has_properties(&self) -> bool {
212        self.blocks.iter().any(|block| !block.properties.is_empty())
213    }
214
215    fn has_property(&self, name: &str) -> bool {
216        self.blocks.iter().any(|block| {
217            block
218                .properties
219                .iter()
220                .any(|property| property.get_name() == name)
221        })
222    }
223
224    fn has_property_value(&self, name: &str, value: &str) -> bool {
225        self.blocks.iter().any(|block| {
226            block.properties.iter().any(|property| {
227                property.get_name() == name && property.get_possible_value_names().contains(&value)
228            })
229        })
230    }
231
232    fn has_block_entity(&self) -> bool {
233        self.blocks
234            .iter()
235            .any(|block| block.default_state().has_block_entity())
236    }
237}
238
239fn parse_suggestion_identifier(input: &str) -> Option<Identifier> {
240    let mut reader = StringReader::new(input);
241    let identifier = parse_identifier(&mut reader).ok()?;
242    (!reader.can_read()).then_some(identifier)
243}
244
245fn parsed_command_string(input: &str) -> Option<String> {
246    let mut reader = StringReader::new(input);
247    reader.skip_whitespace();
248    let value = reader.read_string().ok()?;
249    reader.skip_whitespace();
250    (!reader.can_read()).then_some(value)
251}
252
253fn completed_property_names(input: &str) -> Vec<String> {
254    let mut names = Vec::new();
255    let mut start = 0;
256    while let Some(separator) = find_unquoted(&input[start..], ',') {
257        let end = start + separator;
258        let property = &input[start..end];
259        if let Some(equals) = find_unquoted(property, '=')
260            && let Some(name) = parsed_command_string(&property[..equals])
261        {
262            names.push(name);
263        }
264        start = end + 1;
265    }
266    names
267}
268
269fn current_property_start(input: &str) -> usize {
270    let mut start = 0;
271    while let Some(separator) = find_unquoted(&input[start..], ',') {
272        start += separator + 1;
273    }
274    start
275}
276
277fn find_unquoted(input: &str, needle: char) -> Option<usize> {
278    let mut quote = None;
279    let mut escaped = false;
280    for (index, character) in input.char_indices() {
281        if let Some(terminator) = quote {
282            if escaped {
283                escaped = false;
284            } else if character == '\\' {
285                escaped = true;
286            } else if character == terminator {
287                quote = None;
288            }
289            continue;
290        }
291        match character {
292            '\'' | '"' => quote = Some(character),
293            _ if character == needle => return Some(index),
294            _ => {}
295        }
296    }
297    None
298}