1use std::{error::Error, fmt};
4
5use steel_utils::translations;
6use text_components::{Modifier, TextComponent, format::Color, interactivity::ClickEvent};
7
8const CONTEXT_AMOUNT: usize = 10;
9
10#[derive(Clone, Debug, PartialEq)]
12pub(crate) enum CommandSyntaxErrorKind {
13 UnknownCommand,
15 UnknownArgument,
17 Dynamic(Box<TextComponent>),
19 ExpectedStartOfQuote,
21 ExpectedEndOfQuote,
23 InvalidEscape(char),
25 InvalidBool(Box<str>),
27 InvalidInt(Box<str>),
29 ExpectedInt,
31 InvalidLong(Box<str>),
33 ExpectedLong,
35 InvalidDouble(Box<str>),
37 ExpectedDouble,
39 InvalidFloat(Box<str>),
41 ExpectedFloat,
43 ExpectedBool,
45 ExpectedSymbol(char),
47 LiteralIncorrect(Box<str>),
49 IntegerTooLow { found: i32, minimum: i32 },
51 IntegerTooHigh { found: i32, maximum: i32 },
53 LongTooLow { found: i64, minimum: i64 },
55 LongTooHigh { found: i64, maximum: i64 },
57 FloatTooLow { found: f32, minimum: f32 },
59 FloatTooHigh { found: f32, maximum: f32 },
61 DoubleTooLow { found: f64, minimum: f64 },
63 DoubleTooHigh { found: f64, maximum: f64 },
65 ExpectedArgumentSeparator,
67}
68
69impl fmt::Display for CommandSyntaxErrorKind {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Self::UnknownCommand => formatter.write_str("Unknown command"),
73 Self::UnknownArgument => formatter.write_str("Incorrect argument for command"),
74 Self::Dynamic(message) => write!(formatter, "{message}"),
75 Self::ExpectedStartOfQuote => formatter.write_str("Expected quote to start a string"),
76 Self::ExpectedEndOfQuote => formatter.write_str("Unclosed quoted string"),
77 Self::InvalidEscape(character) => write!(
78 formatter,
79 "Invalid escape sequence '{character}' in quoted string"
80 ),
81 Self::InvalidBool(value) => write!(
82 formatter,
83 "Invalid bool, expected true or false but found '{value}'"
84 ),
85 Self::InvalidInt(value) => write!(formatter, "Invalid integer '{value}'"),
86 Self::ExpectedInt => formatter.write_str("Expected integer"),
87 Self::InvalidLong(value) => write!(formatter, "Invalid long '{value}'"),
88 Self::ExpectedLong => formatter.write_str("Expected long"),
89 Self::InvalidDouble(value) => write!(formatter, "Invalid double '{value}'"),
90 Self::ExpectedDouble => formatter.write_str("Expected double"),
91 Self::InvalidFloat(value) => write!(formatter, "Invalid float '{value}'"),
92 Self::ExpectedFloat => formatter.write_str("Expected float"),
93 Self::ExpectedBool => formatter.write_str("Expected bool"),
94 Self::ExpectedSymbol(symbol) => write!(formatter, "Expected '{symbol}'"),
95 Self::LiteralIncorrect(expected) => write!(formatter, "Expected literal {expected}"),
96 Self::IntegerTooLow { found, minimum } => write!(
97 formatter,
98 "Integer must not be less than {minimum}, found {found}"
99 ),
100 Self::IntegerTooHigh { found, maximum } => write!(
101 formatter,
102 "Integer must not be more than {maximum}, found {found}"
103 ),
104 Self::LongTooLow { found, minimum } => write!(
105 formatter,
106 "Long must not be less than {minimum}, found {found}"
107 ),
108 Self::LongTooHigh { found, maximum } => write!(
109 formatter,
110 "Long must not be more than {maximum}, found {found}"
111 ),
112 Self::FloatTooLow { found, minimum } => write!(
113 formatter,
114 "Float must not be less than {minimum}, found {found}"
115 ),
116 Self::FloatTooHigh { found, maximum } => write!(
117 formatter,
118 "Float must not be more than {maximum}, found {found}"
119 ),
120 Self::DoubleTooLow { found, minimum } => write!(
121 formatter,
122 "Double must not be less than {minimum}, found {found}"
123 ),
124 Self::DoubleTooHigh { found, maximum } => write!(
125 formatter,
126 "Double must not be more than {maximum}, found {found}"
127 ),
128 Self::ExpectedArgumentSeparator => formatter
129 .write_str("Expected whitespace to end one argument, but found trailing data"),
130 }
131 }
132}
133
134impl CommandSyntaxErrorKind {
135 fn component(&self) -> TextComponent {
136 match self {
137 Self::UnknownCommand => TextComponent::from(&translations::COMMAND_UNKNOWN_COMMAND),
138 Self::UnknownArgument => TextComponent::from(&translations::COMMAND_UNKNOWN_ARGUMENT),
139 Self::Dynamic(message) => message.as_ref().clone(),
140 Self::ExpectedStartOfQuote => {
141 TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_START)
142 }
143 Self::ExpectedEndOfQuote => {
144 TextComponent::from(&translations::PARSING_QUOTE_EXPECTED_END)
145 }
146 Self::InvalidEscape(character) => translations::PARSING_QUOTE_ESCAPE
147 .message([character.to_string()])
148 .component(),
149 Self::InvalidBool(value) => translations::PARSING_BOOL_INVALID
150 .message([value.to_string()])
151 .component(),
152 Self::InvalidInt(value) => translations::PARSING_INT_INVALID
153 .message([value.to_string()])
154 .component(),
155 Self::ExpectedInt => TextComponent::from(&translations::PARSING_INT_EXPECTED),
156 Self::InvalidLong(value) => translations::PARSING_LONG_INVALID
157 .message([value.to_string()])
158 .component(),
159 Self::ExpectedLong => TextComponent::from(&translations::PARSING_LONG_EXPECTED),
160 Self::InvalidDouble(value) => translations::PARSING_DOUBLE_INVALID
161 .message([value.to_string()])
162 .component(),
163 Self::ExpectedDouble => TextComponent::from(&translations::PARSING_DOUBLE_EXPECTED),
164 Self::InvalidFloat(value) => translations::PARSING_FLOAT_INVALID
165 .message([value.to_string()])
166 .component(),
167 Self::ExpectedFloat => TextComponent::from(&translations::PARSING_FLOAT_EXPECTED),
168 Self::ExpectedBool => TextComponent::from(&translations::PARSING_BOOL_EXPECTED),
169 Self::ExpectedSymbol(symbol) => translations::PARSING_EXPECTED
170 .message([symbol.to_string()])
171 .component(),
172 Self::LiteralIncorrect(expected) => translations::ARGUMENT_LITERAL_INCORRECT
173 .message([expected.to_string()])
174 .component(),
175 Self::IntegerTooLow { found, minimum } => translations::ARGUMENT_INTEGER_LOW
176 .message([minimum.to_string(), found.to_string()])
177 .component(),
178 Self::IntegerTooHigh { found, maximum } => translations::ARGUMENT_INTEGER_BIG
179 .message([maximum.to_string(), found.to_string()])
180 .component(),
181 Self::LongTooLow { found, minimum } => translations::ARGUMENT_LONG_LOW
182 .message([minimum.to_string(), found.to_string()])
183 .component(),
184 Self::LongTooHigh { found, maximum } => translations::ARGUMENT_LONG_BIG
185 .message([maximum.to_string(), found.to_string()])
186 .component(),
187 Self::FloatTooLow { found, minimum } => translations::ARGUMENT_FLOAT_LOW
188 .message([minimum.to_string(), found.to_string()])
189 .component(),
190 Self::FloatTooHigh { found, maximum } => translations::ARGUMENT_FLOAT_BIG
191 .message([maximum.to_string(), found.to_string()])
192 .component(),
193 Self::DoubleTooLow { found, minimum } => translations::ARGUMENT_DOUBLE_LOW
194 .message([minimum.to_string(), found.to_string()])
195 .component(),
196 Self::DoubleTooHigh { found, maximum } => translations::ARGUMENT_DOUBLE_BIG
197 .message([maximum.to_string(), found.to_string()])
198 .component(),
199 Self::ExpectedArgumentSeparator => {
200 TextComponent::from(&translations::COMMAND_EXPECTED_SEPARATOR)
201 }
202 }
203 }
204}
205
206#[derive(Clone, Debug, PartialEq)]
211pub(crate) struct CommandSyntaxError {
212 kind: CommandSyntaxErrorKind,
213 context: Option<CommandErrorContext>,
214}
215
216#[derive(Clone, Debug, PartialEq)]
217struct CommandErrorContext {
218 input: Box<str>,
219 cursor: usize,
220 byte_cursor: usize,
221}
222
223impl CommandSyntaxError {
224 pub(super) fn new(
225 kind: CommandSyntaxErrorKind,
226 input: &str,
227 cursor: usize,
228 byte_cursor: usize,
229 ) -> Self {
230 Self {
231 kind,
232 context: Some(CommandErrorContext {
233 input: input.into(),
234 cursor,
235 byte_cursor,
236 }),
237 }
238 }
239
240 pub(crate) fn dynamic(message: impl Into<TextComponent>) -> Self {
242 Self {
243 kind: CommandSyntaxErrorKind::Dynamic(Box::new(message.into())),
244 context: None,
245 }
246 }
247
248 pub(crate) const fn kind(&self) -> &CommandSyntaxErrorKind {
250 &self.kind
251 }
252
253 pub(crate) fn input(&self) -> Option<&str> {
255 self.context.as_ref().map(|context| context.input.as_ref())
256 }
257
258 pub(crate) const fn cursor(&self) -> Option<usize> {
260 match &self.context {
261 Some(context) => Some(context.cursor),
262 None => None,
263 }
264 }
265
266 pub(crate) fn raw_message(&self) -> String {
268 self.kind.to_string()
269 }
270
271 pub(crate) fn message_component(&self) -> TextComponent {
273 self.kind.component()
274 }
275
276 pub(crate) fn context_component(&self) -> Option<TextComponent> {
278 let context = self.context.as_ref()?;
279 let input_before_cursor = &context.input[..context.byte_cursor];
280 let mut context_start = context.byte_cursor;
281 let mut context_length = 0;
282
283 for (byte_index, character) in input_before_cursor.char_indices().rev() {
284 let character_length = character.len_utf16();
285 if context_length + character_length > CONTEXT_AMOUNT {
286 break;
287 }
288 context_length += character_length;
289 context_start = byte_index;
290 }
291
292 let suggested_command = if context.input.starts_with('/') {
293 context.input.to_string()
294 } else {
295 format!("/{}", context.input)
296 };
297 let mut component = TextComponent::new()
298 .color(Color::Gray)
299 .click_event(ClickEvent::suggest_command(suggested_command));
300 if context.cursor > CONTEXT_AMOUNT {
301 component = component.add_child(TextComponent::const_plain("..."));
302 }
303 component = component.add_child(TextComponent::plain(
304 context.input[context_start..context.byte_cursor].to_owned(),
305 ));
306 if context.byte_cursor < context.input.len() {
307 component = component.add_child(
308 TextComponent::plain(context.input[context.byte_cursor..].to_owned())
309 .color(Color::Red)
310 .underlined(true),
311 );
312 }
313 Some(
314 component.add_child(
315 TextComponent::from(&translations::COMMAND_CONTEXT_HERE)
316 .color(Color::Red)
317 .italic(true),
318 ),
319 )
320 }
321
322 pub(crate) fn context(&self) -> Option<String> {
324 let context = self.context.as_ref()?;
325 let input_before_cursor = &context.input[..context.byte_cursor];
326 let mut context_start = context.byte_cursor;
327 let mut context_length = 0;
328
329 for (byte_index, character) in input_before_cursor.char_indices().rev() {
330 let character_length = character.len_utf16();
331 if context_length + character_length > CONTEXT_AMOUNT {
332 break;
333 }
334 context_length += character_length;
335 context_start = byte_index;
336 }
337
338 let prefix = if context.cursor > CONTEXT_AMOUNT {
339 "..."
340 } else {
341 ""
342 };
343 Some(format!(
344 "{prefix}{}<--[HERE]",
345 &context.input[context_start..context.byte_cursor]
346 ))
347 }
348}
349
350impl fmt::Display for CommandSyntaxError {
351 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
352 let Some(context) = &self.context else {
353 return self.kind.fmt(formatter);
354 };
355 let Some(display_context) = self.context() else {
356 return self.kind.fmt(formatter);
357 };
358 write!(
359 formatter,
360 "{} at position {}: {display_context}",
361 self.kind, context.cursor
362 )
363 }
364}
365
366impl Error for CommandSyntaxError {}