steel_core/command/brigadier/
suggestion.rs1use std::{cmp::Ordering, ops::Range};
4
5use text_components::TextComponent;
6use thiserror::Error;
7
8use super::StringRange;
9
10#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub(crate) enum SuggestionError {
13 #[error(
15 "suggestion range {range:?} is invalid for input containing {input_length} UTF-16 code units"
16 )]
17 InvalidRange {
18 range: StringRange,
19 input_length: usize,
20 },
21 #[error("suggestion range {outer:?} does not encompass {inner:?}")]
23 NonEncompassingRange {
24 outer: StringRange,
25 inner: StringRange,
26 },
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Hash)]
31pub(crate) struct Suggestion {
32 range: StringRange,
33 text: Box<str>,
34 tooltip: Option<TextComponent>,
35 integer: Option<i32>,
36}
37
38impl Suggestion {
39 pub(crate) fn new(range: StringRange, text: impl Into<Box<str>>) -> Self {
41 Self {
42 range,
43 text: text.into(),
44 tooltip: None,
45 integer: None,
46 }
47 }
48
49 pub(crate) fn with_tooltip(
51 range: StringRange,
52 text: impl Into<Box<str>>,
53 tooltip: impl Into<TextComponent>,
54 ) -> Self {
55 Self {
56 range,
57 text: text.into(),
58 tooltip: Some(tooltip.into()),
59 integer: None,
60 }
61 }
62
63 fn integer(range: StringRange, value: i32) -> Self {
64 Self {
65 range,
66 text: value.to_string().into(),
67 tooltip: None,
68 integer: Some(value),
69 }
70 }
71
72 pub(crate) const fn range(&self) -> StringRange {
74 self.range
75 }
76
77 pub(crate) fn text(&self) -> &str {
79 &self.text
80 }
81
82 pub(crate) const fn tooltip(&self) -> Option<&TextComponent> {
84 self.tooltip.as_ref()
85 }
86
87 pub(crate) fn apply(&self, input: &str) -> Result<String, SuggestionError> {
89 let range = Self::checked_byte_range(input, self.range)?;
90 let mut result = String::with_capacity(input.len() - range.len() + self.text.len());
91 result.push_str(&input[..range.start]);
92 result.push_str(&self.text);
93 result.push_str(&input[range.end..]);
94 Ok(result)
95 }
96
97 fn expand(&self, input: &str, range: StringRange) -> Result<Self, SuggestionError> {
98 if range.start() > self.range.start() || range.end() < self.range.end() {
99 return Err(SuggestionError::NonEncompassingRange {
100 outer: range,
101 inner: self.range,
102 });
103 }
104 if range == self.range {
105 return Ok(self.clone());
106 }
107
108 let prefix = Self::checked_byte_range(
109 input,
110 StringRange::between(range.start(), self.range.start()),
111 )?;
112 let suffix =
113 Self::checked_byte_range(input, StringRange::between(self.range.end(), range.end()))?;
114 let mut text = String::with_capacity(prefix.len() + self.text.len() + suffix.len());
115 text.push_str(&input[prefix]);
116 text.push_str(&self.text);
117 text.push_str(&input[suffix]);
118
119 Ok(Self {
120 range,
121 text: text.into_boxed_str(),
122 tooltip: self.tooltip.clone(),
123 integer: None,
126 })
127 }
128
129 fn checked_byte_range(
130 input: &str,
131 range: StringRange,
132 ) -> Result<Range<usize>, SuggestionError> {
133 range
134 .byte_range(input)
135 .ok_or(SuggestionError::InvalidRange {
136 range,
137 input_length: input.encode_utf16().count(),
138 })
139 }
140
141 fn compare_ignore_case(&self, other: &Self) -> Ordering {
142 match (self.integer, other.integer) {
143 (Some(first), Some(second)) => first.cmp(&second),
144 _ => self.text.to_lowercase().cmp(&other.text.to_lowercase()),
145 }
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
151pub(crate) struct Suggestions {
152 range: StringRange,
153 suggestions: Vec<Suggestion>,
154}
155
156impl Suggestions {
157 pub(crate) const fn new(range: StringRange, suggestions: Vec<Suggestion>) -> Self {
159 Self { range, suggestions }
160 }
161
162 pub(crate) const fn empty() -> Self {
164 Self {
165 range: StringRange::at(0),
166 suggestions: Vec::new(),
167 }
168 }
169
170 pub(crate) const fn range(&self) -> StringRange {
172 self.range
173 }
174
175 pub(crate) fn list(&self) -> &[Suggestion] {
177 &self.suggestions
178 }
179
180 pub(crate) const fn is_empty(&self) -> bool {
182 self.suggestions.is_empty()
183 }
184
185 pub(crate) fn merge(input: &str, suggestions: Vec<Self>) -> Result<Self, SuggestionError> {
187 let mut suggestions = suggestions.into_iter();
188 let Some(first) = suggestions.next() else {
189 return Ok(Self::empty());
190 };
191 let Some(second) = suggestions.next() else {
192 return Ok(first);
193 };
194
195 let mut merged = first.suggestions;
196 merged.extend(second.suggestions);
197 for suggestions in suggestions {
198 merged.extend(suggestions.suggestions);
199 }
200 Self::create(input, merged)
201 }
202
203 fn create(input: &str, suggestions: Vec<Suggestion>) -> Result<Self, SuggestionError> {
204 if suggestions.is_empty() {
205 return Ok(Self::empty());
206 }
207
208 let mut start = usize::MAX;
209 let mut end = 0;
210 for suggestion in &suggestions {
211 start = start.min(suggestion.range.start());
212 end = end.max(suggestion.range.end());
213 }
214 let range = StringRange::between(start, end);
215 let mut expanded = Vec::with_capacity(suggestions.len());
216 for suggestion in suggestions {
217 let suggestion = suggestion.expand(input, range)?;
218 if !expanded.contains(&suggestion) {
219 expanded.push(suggestion);
220 }
221 }
222 expanded.sort_by(Self::compare_suggestions);
223 Ok(Self::new(range, expanded))
224 }
225
226 fn compare_suggestions(first: &Suggestion, second: &Suggestion) -> Ordering {
227 first.compare_ignore_case(second)
228 }
229}
230
231pub(crate) struct SuggestionsBuilder<'input> {
233 input: &'input str,
234 start: usize,
235 byte_start: usize,
236 remaining_lowercase: String,
237 suggestions: Vec<Suggestion>,
238}
239
240impl<'input> SuggestionsBuilder<'input> {
241 pub(crate) fn new(input: &'input str, start: usize) -> Result<Self, SuggestionError> {
243 let range = StringRange::at(start);
244 let Some(byte_range) = range.byte_range(input) else {
245 return Err(SuggestionError::InvalidRange {
246 range,
247 input_length: input.encode_utf16().count(),
248 });
249 };
250 Ok(Self {
251 input,
252 start,
253 byte_start: byte_range.start,
254 remaining_lowercase: input[byte_range.start..].to_lowercase(),
255 suggestions: Vec::new(),
256 })
257 }
258
259 pub(crate) const fn input(&self) -> &'input str {
261 self.input
262 }
263
264 pub(crate) const fn start(&self) -> usize {
266 self.start
267 }
268
269 pub(crate) fn remaining(&self) -> &'input str {
271 &self.input[self.byte_start..]
272 }
273
274 pub(crate) fn remaining_lowercase(&self) -> &str {
276 &self.remaining_lowercase
277 }
278
279 pub(crate) fn suggest(&mut self, text: impl Into<Box<str>>) -> &mut Self {
281 let text = text.into();
282 if text.as_ref() != self.remaining() {
283 self.suggestions.push(Suggestion::new(self.range(), text));
284 }
285 self
286 }
287
288 pub(crate) fn suggest_with_tooltip(
290 &mut self,
291 text: impl Into<Box<str>>,
292 tooltip: impl Into<TextComponent>,
293 ) -> &mut Self {
294 let text = text.into();
295 if text.as_ref() != self.remaining() {
296 self.suggestions
297 .push(Suggestion::with_tooltip(self.range(), text, tooltip));
298 }
299 self
300 }
301
302 pub(crate) fn suggest_integer(&mut self, value: i32) -> &mut Self {
304 self.suggestions
305 .push(Suggestion::integer(self.range(), value));
306 self
307 }
308
309 pub(crate) fn build(self) -> Result<Suggestions, SuggestionError> {
311 Suggestions::create(self.input, self.suggestions)
312 }
313
314 pub(crate) fn restart(&self) -> Result<Self, SuggestionError> {
316 Self::new(self.input, self.start)
317 }
318
319 fn range(&self) -> StringRange {
320 StringRange::between(self.start, self.input.encode_utf16().count())
321 }
322}