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