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::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 = ¤t[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(¤t[..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 = ¤t[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}