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