From 79ad1e93bf6ce2b7c2da6925a7c85b33bb76f154 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 12 Oct 2023 22:01:15 -0500 Subject: [PATCH] brigadier suggestions closes #109 --- .../src/arguments/argument_type.rs | 14 +- .../src/arguments/bool_argument_type.rs | 19 +- .../src/arguments/double_argument_type.rs | 7 + .../src/arguments/float_argument_type.rs | 7 + .../src/arguments/integer_argument_type.rs | 7 + .../src/arguments/long_argument_type.rs | 7 + .../src/arguments/string_argument_type.rs | 11 + .../src/builder/required_argument_builder.rs | 16 +- azalea-brigadier/src/command_dispatcher.rs | 36 ++ .../src/context/command_context_builder.rs | 39 +- azalea-brigadier/src/context/mod.rs | 1 + .../src/context/suggestion_context.rs | 11 + azalea-brigadier/src/suggestion/mod.rs | 24 +- .../src/suggestion/suggestions.rs | 36 +- .../src/suggestion/suggestions_builder.rs | 24 +- azalea-brigadier/src/tree/mod.rs | 24 + .../arguments/bool_argument_type_test.rs | 1 + .../arguments/double_argument_type_test.rs | 1 + .../arguments/float_argument_type_test.rs | 1 + .../arguments/integer_argument_type_test.rs | 1 + .../arguments/long_argument_type_test.rs | 1 + .../arguments/string_argument_type_test.rs | 1 + .../builder/literal_argument_builder_test.rs | 1 + .../builder/required_argument_builder_test.rs | 1 + .../tests/command_dispatcher_usages_test.rs | 53 ++- .../tests/command_suggestions_test.rs | 445 ++++++++++++++++++ .../tests/context/command_context_test.rs | 1 + .../tests/context/parsed_argument_test.rs | 1 + ...amic_command_syntax_exception_type_test.rs | 1 + ...mple_command_syntax_exception_type_test.rs | 1 + azalea-brigadier/tests/suggestion/mod.rs | 2 +- .../tests/suggestion/suggestions_test.rs | 2 +- 32 files changed, 738 insertions(+), 59 deletions(-) create mode 100644 azalea-brigadier/src/context/suggestion_context.rs diff --git a/azalea-brigadier/src/arguments/argument_type.rs b/azalea-brigadier/src/arguments/argument_type.rs index f44233e1..d7bfa7d6 100755 --- a/azalea-brigadier/src/arguments/argument_type.rs +++ b/azalea-brigadier/src/arguments/argument_type.rs @@ -1,7 +1,19 @@ use std::{any::Any, sync::Arc}; -use crate::{exceptions::CommandSyntaxException, string_reader::StringReader}; +use crate::{ + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, +}; pub trait ArgumentType { fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException>; + + fn list_suggestions(&self, _builder: SuggestionsBuilder) -> Suggestions { + Suggestions::default() + } + + fn examples(&self) -> Vec { + vec![] + } } diff --git a/azalea-brigadier/src/arguments/bool_argument_type.rs b/azalea-brigadier/src/arguments/bool_argument_type.rs index 2e348c7b..a73a9da5 100644 --- a/azalea-brigadier/src/arguments/bool_argument_type.rs +++ b/azalea-brigadier/src/arguments/bool_argument_type.rs @@ -1,7 +1,10 @@ use std::{any::Any, sync::Arc}; use crate::{ - context::CommandContext, exceptions::CommandSyntaxException, string_reader::StringReader, + context::CommandContext, + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, }; use super::ArgumentType; @@ -13,6 +16,20 @@ impl ArgumentType for Boolean { fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException> { Ok(Arc::new(reader.read_boolean()?)) } + + fn list_suggestions(&self, mut builder: SuggestionsBuilder) -> Suggestions { + if "true".starts_with(builder.remaining_lowercase()) { + builder = builder.suggest("true"); + } + if "false".starts_with(builder.remaining_lowercase()) { + builder = builder.suggest("false"); + } + builder.build() + } + + fn examples(&self) -> Vec { + vec!["true".to_string(), "false".to_string()] + } } pub fn bool() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/double_argument_type.rs b/azalea-brigadier/src/arguments/double_argument_type.rs index 9502a680..ea99f1cf 100644 --- a/azalea-brigadier/src/arguments/double_argument_type.rs +++ b/azalea-brigadier/src/arguments/double_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Double { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "1.2", ".5", "-1", "-.5", "-1234.56"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn double() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/float_argument_type.rs b/azalea-brigadier/src/arguments/float_argument_type.rs index a2831a08..2333499a 100644 --- a/azalea-brigadier/src/arguments/float_argument_type.rs +++ b/azalea-brigadier/src/arguments/float_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Float { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "1.2", ".5", "-1", "-.5", "-1234.56"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn float() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/integer_argument_type.rs b/azalea-brigadier/src/arguments/integer_argument_type.rs index a31a6e70..cc4755ee 100644 --- a/azalea-brigadier/src/arguments/integer_argument_type.rs +++ b/azalea-brigadier/src/arguments/integer_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Integer { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "123", "-123"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn integer() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/long_argument_type.rs b/azalea-brigadier/src/arguments/long_argument_type.rs index d557881a..4e36abee 100644 --- a/azalea-brigadier/src/arguments/long_argument_type.rs +++ b/azalea-brigadier/src/arguments/long_argument_type.rs @@ -40,6 +40,13 @@ impl ArgumentType for Long { } Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + vec!["0", "123", "-123"] + .into_iter() + .map(|s| s.to_string()) + .collect() + } } pub fn long() -> impl ArgumentType { diff --git a/azalea-brigadier/src/arguments/string_argument_type.rs b/azalea-brigadier/src/arguments/string_argument_type.rs index 9fd70d13..d38fbc79 100644 --- a/azalea-brigadier/src/arguments/string_argument_type.rs +++ b/azalea-brigadier/src/arguments/string_argument_type.rs @@ -29,6 +29,17 @@ impl ArgumentType for StringArgument { }; Ok(Arc::new(result)) } + + fn examples(&self) -> Vec { + match self { + StringArgument::SingleWord => vec!["word", "words_with_underscores"], + StringArgument::QuotablePhrase => vec!["\"quoted phrase\"", "word", "\"\""], + StringArgument::GreedyPhrase => vec!["word", "words with spaces", "\"and symbols\""], + } + .into_iter() + .map(|s| s.to_string()) + .collect() + } } /// Match up until the next space. diff --git a/azalea-brigadier/src/builder/required_argument_builder.rs b/azalea-brigadier/src/builder/required_argument_builder.rs index 60fa713f..1c79f619 100755 --- a/azalea-brigadier/src/builder/required_argument_builder.rs +++ b/azalea-brigadier/src/builder/required_argument_builder.rs @@ -1,6 +1,9 @@ use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType}; use crate::{ - arguments::ArgumentType, exceptions::CommandSyntaxException, string_reader::StringReader, + arguments::ArgumentType, + exceptions::CommandSyntaxException, + string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, }; use std::{any::Any, fmt::Debug, sync::Arc}; @@ -22,6 +25,17 @@ impl Argument { pub fn parse(&self, reader: &mut StringReader) -> Result, CommandSyntaxException> { self.parser.parse(reader) } + + pub fn list_suggestions(&self, builder: SuggestionsBuilder) -> Suggestions { + // TODO: custom suggestions + // https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java#L71 + + self.parser.list_suggestions(builder) + } + + pub fn examples(&self) -> Vec { + self.parser.examples() + } } impl From for ArgumentBuilderType { diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index 0ec07a54..3b5987fc 100755 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -6,6 +6,7 @@ use crate::{ exceptions::{BuiltInExceptions, CommandSyntaxException}, parse_results::ParseResults, string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, tree::CommandNode, }; use std::{ @@ -474,6 +475,41 @@ impl CommandDispatcher { Some(this) } + + pub fn get_completion_suggestions(parse: ParseResults) -> Suggestions { + let cursor = parse.reader.total_length(); + Self::get_completion_suggestions_with_cursor(parse, cursor) + } + + pub fn get_completion_suggestions_with_cursor( + parse: ParseResults, + cursor: usize, + ) -> Suggestions { + let context = parse.context; + + let node_before_cursor = context.find_suggestion_context(cursor); + let parent = node_before_cursor.parent; + let start = usize::min(node_before_cursor.start_pos, cursor); + + let full_input = parse.reader.string(); + let truncated_input = full_input[..cursor].to_string(); + let truncated_input_lowercase = truncated_input.to_lowercase(); + + let mut all_suggestions = Vec::new(); + for node in parent.read().children.values() { + let suggestions = node.read().list_suggestions( + context.build(&truncated_input), + SuggestionsBuilder::new_with_lowercase( + &truncated_input, + &truncated_input_lowercase, + start, + ), + ); + all_suggestions.push(suggestions); + } + + Suggestions::merge(full_input, &all_suggestions) + } } impl Default for CommandDispatcher { diff --git a/azalea-brigadier/src/context/command_context_builder.rs b/azalea-brigadier/src/context/command_context_builder.rs index 99c40dac..75295825 100755 --- a/azalea-brigadier/src/context/command_context_builder.rs +++ b/azalea-brigadier/src/context/command_context_builder.rs @@ -2,7 +2,7 @@ use parking_lot::RwLock; use super::{ command_context::CommandContext, parsed_command_node::ParsedCommandNode, - string_range::StringRange, ParsedArgument, + string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument, }; use crate::{ command_dispatcher::CommandDispatcher, @@ -99,6 +99,43 @@ impl<'a, S> CommandContextBuilder<'a, S> { input: input.to_string(), } } + + pub fn find_suggestion_context(&self, cursor: usize) -> SuggestionContext { + if self.range.start() > cursor { + panic!("Can't find node before cursor"); + } + + if self.range.end() < cursor { + if let Some(child) = &self.child { + child.find_suggestion_context(cursor) + } else if let Some(last) = self.nodes.last() { + SuggestionContext { + parent: Arc::clone(&last.node), + start_pos: last.range.end() + 1, + } + } else { + SuggestionContext { + parent: Arc::clone(&self.root), + start_pos: self.range.start(), + } + } + } else { + let mut prev = &self.root; + for node in &self.nodes { + if node.range.start() <= cursor && cursor <= node.range.end() { + return SuggestionContext { + parent: Arc::clone(prev), + start_pos: node.range.start(), + }; + } + prev = &node.node; + } + SuggestionContext { + parent: Arc::clone(prev), + start_pos: self.range.start(), + } + } + } } impl Debug for CommandContextBuilder<'_, S> { diff --git a/azalea-brigadier/src/context/mod.rs b/azalea-brigadier/src/context/mod.rs index d535602a..28e1a12e 100755 --- a/azalea-brigadier/src/context/mod.rs +++ b/azalea-brigadier/src/context/mod.rs @@ -3,6 +3,7 @@ mod command_context_builder; mod parsed_argument; mod parsed_command_node; mod string_range; +pub mod suggestion_context; pub use command_context::CommandContext; pub use command_context_builder::CommandContextBuilder; diff --git a/azalea-brigadier/src/context/suggestion_context.rs b/azalea-brigadier/src/context/suggestion_context.rs new file mode 100644 index 00000000..58a73fdb --- /dev/null +++ b/azalea-brigadier/src/context/suggestion_context.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use parking_lot::RwLock; + +use crate::tree::CommandNode; + +#[derive(Debug)] +pub struct SuggestionContext { + pub parent: Arc>>, + pub start_pos: usize, +} diff --git a/azalea-brigadier/src/suggestion/mod.rs b/azalea-brigadier/src/suggestion/mod.rs index bc0e7608..fbebfe8a 100755 --- a/azalea-brigadier/src/suggestion/mod.rs +++ b/azalea-brigadier/src/suggestion/mod.rs @@ -20,13 +20,10 @@ pub use suggestions_builder::SuggestionsBuilder; /// The `M` generic is the type of the tooltip, so for example a `String` or /// just `()` if you don't care about it. #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Suggestion -where - M: Clone, -{ +pub struct Suggestion { pub range: StringRange, value: SuggestionValue, - pub tooltip: Option, + pub tooltip: Option, } #[derive(Debug, Clone, Hash, Eq, PartialEq)] @@ -35,18 +32,16 @@ pub enum SuggestionValue { Text(String), } -impl Suggestion<()> { - pub fn new(range: StringRange, text: &str) -> Suggestion<()> { +impl Suggestion { + pub fn new(range: StringRange, text: &str) -> Suggestion { Suggestion { range, value: SuggestionValue::Text(text.to_string()), tooltip: None, } } -} -impl Suggestion { - pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: M) -> Self { + pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: String) -> Self { Self { range, value: SuggestionValue::Text(text.to_string()), @@ -71,7 +66,7 @@ impl Suggestion { result } - pub fn expand(&self, command: &str, range: StringRange) -> Suggestion { + pub fn expand(&self, command: &str, range: StringRange) -> Suggestion { if range == self.range { return self.clone(); } @@ -140,10 +135,13 @@ impl PartialOrd for SuggestionValue { } #[cfg(feature = "azalea-buf")] -impl McBufWritable for Suggestion { +impl McBufWritable for Suggestion { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { self.value.to_string().write_into(buf)?; - self.tooltip.write_into(buf)?; + self.tooltip + .clone() + .map(FormattedText::from) + .write_into(buf)?; Ok(()) } } diff --git a/azalea-brigadier/src/suggestion/suggestions.rs b/azalea-brigadier/src/suggestion/suggestions.rs index a5119d88..487e4233 100755 --- a/azalea-brigadier/src/suggestion/suggestions.rs +++ b/azalea-brigadier/src/suggestion/suggestions.rs @@ -12,21 +12,18 @@ use azalea_chat::FormattedText; use std::io::{Cursor, Write}; use std::{collections::HashSet, hash::Hash}; -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Suggestions -where - M: Clone + PartialEq + Hash, -{ +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +pub struct Suggestions { range: StringRange, - suggestions: Vec>, + suggestions: Vec, } -impl Suggestions { - pub fn new(range: StringRange, suggestions: Vec>) -> Self { +impl Suggestions { + pub fn new(range: StringRange, suggestions: Vec) -> Self { Self { range, suggestions } } - pub fn merge(command: &str, input: &[Suggestions]) -> Self { + pub fn merge(command: &str, input: &[Suggestions]) -> Self { if input.is_empty() { return Suggestions::default(); } else if input.len() == 1 { @@ -41,7 +38,7 @@ impl Suggestions { Suggestions::create(command, &texts) } - pub fn create(command: &str, suggestions: &HashSet>) -> Self { + pub fn create(command: &str, suggestions: &HashSet) -> Self { if suggestions.is_empty() { return Suggestions::default(); }; @@ -70,7 +67,7 @@ impl Suggestions { self.suggestions.is_empty() } - pub fn list(&self) -> &[Suggestion] { + pub fn list(&self) -> &[Suggestion] { &self.suggestions } @@ -79,19 +76,8 @@ impl Suggestions { } } -// this can't be derived because that'd require the generic to have `Default` -// too even if it's not actually necessary -impl Default for Suggestions { - fn default() -> Self { - Self { - range: StringRange::default(), - suggestions: Vec::new(), - } - } -} - #[cfg(feature = "azalea-buf")] -impl McBufReadable for Suggestions { +impl McBufReadable for Suggestions { fn read_from(buf: &mut Cursor<&[u8]>) -> Result { #[derive(McBuf)] struct StandaloneSuggestion { @@ -109,7 +95,7 @@ impl McBufReadable for Suggestions { .into_iter() .map(|s| Suggestion { value: SuggestionValue::Text(s.text), - tooltip: s.tooltip, + tooltip: s.tooltip.map(|t| t.to_string()), range, }) .collect::>(); @@ -120,7 +106,7 @@ impl McBufReadable for Suggestions { } #[cfg(feature = "azalea-buf")] -impl McBufWritable for Suggestions { +impl McBufWritable for Suggestions { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { (self.range.start() as u32).var_write_into(buf)?; (self.range.length() as u32).var_write_into(buf)?; diff --git a/azalea-brigadier/src/suggestion/suggestions_builder.rs b/azalea-brigadier/src/suggestion/suggestions_builder.rs index 469a7b98..4e6296dd 100755 --- a/azalea-brigadier/src/suggestion/suggestions_builder.rs +++ b/azalea-brigadier/src/suggestion/suggestions_builder.rs @@ -1,24 +1,20 @@ use std::collections::HashSet; -use std::hash::Hash; use crate::context::StringRange; use super::{Suggestion, SuggestionValue, Suggestions}; #[derive(PartialEq, Debug)] -pub struct SuggestionsBuilder -where - M: Clone + Eq + Hash, -{ +pub struct SuggestionsBuilder { input: String, input_lowercase: String, start: usize, remaining: String, remaining_lowercase: String, - result: HashSet>, + result: HashSet, } -impl SuggestionsBuilder<()> { +impl SuggestionsBuilder { pub fn new(input: &str, start: usize) -> Self { Self::new_with_lowercase(input, input.to_lowercase().as_str(), start) } @@ -35,7 +31,7 @@ impl SuggestionsBuilder<()> { } } -impl SuggestionsBuilder { +impl SuggestionsBuilder { pub fn input(&self) -> &str { &self.input } @@ -52,7 +48,7 @@ impl SuggestionsBuilder { &self.remaining_lowercase } - pub fn build(&self) -> Suggestions { + pub fn build(&self) -> Suggestions { Suggestions::create(&self.input, &self.result) } @@ -68,7 +64,7 @@ impl SuggestionsBuilder { self } - pub fn suggest_with_tooltip(mut self, text: &str, tooltip: M) -> Self { + pub fn suggest_with_tooltip(mut self, text: &str, tooltip: String) -> Self { if text == self.remaining { return self; } @@ -89,7 +85,7 @@ impl SuggestionsBuilder { self } - pub fn suggest_integer_with_tooltip(mut self, value: i32, tooltip: M) -> Self { + pub fn suggest_integer_with_tooltip(mut self, value: i32, tooltip: String) -> Self { self.result.insert(Suggestion { range: StringRange::between(self.start, self.input.len()), value: SuggestionValue::Integer(value), @@ -99,16 +95,16 @@ impl SuggestionsBuilder { } #[allow(clippy::should_implement_trait)] - pub fn add(mut self, other: SuggestionsBuilder) -> Self { + pub fn add(mut self, other: SuggestionsBuilder) -> Self { self.result.extend(other.result); self } - pub fn create_offset(&self, start: usize) -> SuggestionsBuilder<()> { + pub fn create_offset(&self, start: usize) -> SuggestionsBuilder { SuggestionsBuilder::new_with_lowercase(&self.input, &self.input_lowercase, start) } - pub fn restart(&self) -> SuggestionsBuilder<()> { + pub fn restart(&self) -> SuggestionsBuilder { self.create_offset(self.start) } } diff --git a/azalea-brigadier/src/tree/mod.rs b/azalea-brigadier/src/tree/mod.rs index be2efef1..a2b1f38a 100755 --- a/azalea-brigadier/src/tree/mod.rs +++ b/azalea-brigadier/src/tree/mod.rs @@ -9,6 +9,7 @@ use crate::{ exceptions::{BuiltInExceptions, CommandSyntaxException}, modifier::RedirectModifier, string_reader::StringReader, + suggestion::{Suggestions, SuggestionsBuilder}, }; use std::{ collections::{BTreeMap, HashMap}, @@ -209,6 +210,29 @@ impl CommandNode { } None } + + pub fn list_suggestions( + &self, + // context is here because that's how it is in mojang's brigadier, but we haven't + // implemented custom suggestions yet so this is unused rn + _context: CommandContext, + builder: SuggestionsBuilder, + ) -> Suggestions { + match &self.value { + ArgumentBuilderType::Literal(literal) => { + if literal + .value + .to_lowercase() + .starts_with(builder.remaining_lowercase()) + { + builder.suggest(&literal.value).build() + } else { + Suggestions::default() + } + } + ArgumentBuilderType::Argument(argument) => argument.list_suggestions(builder), + } + } } impl Debug for CommandNode { diff --git a/azalea-brigadier/tests/arguments/bool_argument_type_test.rs b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/bool_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/bool_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/double_argument_type_test.rs b/azalea-brigadier/tests/arguments/double_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/double_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/double_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/float_argument_type_test.rs b/azalea-brigadier/tests/arguments/float_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/float_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/float_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/integer_argument_type_test.rs b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/integer_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/integer_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/long_argument_type_test.rs b/azalea-brigadier/tests/arguments/long_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/long_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/long_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/arguments/string_argument_type_test.rs b/azalea-brigadier/tests/arguments/string_argument_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/arguments/string_argument_type_test.rs +++ b/azalea-brigadier/tests/arguments/string_argument_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/builder/literal_argument_builder_test.rs b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/builder/literal_argument_builder_test.rs +++ b/azalea-brigadier/tests/builder/literal_argument_builder_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/builder/required_argument_builder_test.rs b/azalea-brigadier/tests/builder/required_argument_builder_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/builder/required_argument_builder_test.rs +++ b/azalea-brigadier/tests/builder/required_argument_builder_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/command_dispatcher_usages_test.rs b/azalea-brigadier/tests/command_dispatcher_usages_test.rs index 0464dea7..a80a2a97 100755 --- a/azalea-brigadier/tests/command_dispatcher_usages_test.rs +++ b/azalea-brigadier/tests/command_dispatcher_usages_test.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, sync::Arc}; -use azalea_brigadier::{prelude::*, tree::CommandNode}; +use azalea_brigadier::{prelude::*, string_reader::StringReader, tree::CommandNode}; use parking_lot::RwLock; fn setup() -> CommandDispatcher<()> { @@ -141,3 +141,54 @@ fn test_smart_usage_root() { assert_eq!(actual, expected); } + +#[test] +fn test_smart_usage_h() { + let subject = setup(); + let results = subject.get_smart_usage(&get(&subject, "h").read(), &()); + + let actual = results + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v)) + .collect::>(); + + let expected = vec![ + (get(&subject, "h 1"), "[1] i"), + (get(&subject, "h 2"), "[2] i ii"), + (get(&subject, "h 3"), "[3]"), + ]; + + let expected = expected + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v.to_owned())) + .collect::>(); + + assert_eq!(actual, expected); +} + +#[test] +fn test_smart_usage_offset_h() { + let subject = setup(); + let mut offset_h = StringReader::from("/|/|/h"); + offset_h.cursor = 5; + + let results = subject.get_smart_usage(&get(&subject, "h").read(), &()); + + let actual = results + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v)) + .collect::>(); + + let expected = vec![ + (get(&subject, "h 1"), "[1] i"), + (get(&subject, "h 2"), "[2] i ii"), + (get(&subject, "h 3"), "[3]"), + ]; + + let expected = expected + .into_iter() + .map(|(k, v)| (k.read().name().to_owned(), v.to_owned())) + .collect::>(); + + assert_eq!(actual, expected); +} diff --git a/azalea-brigadier/tests/command_suggestions_test.rs b/azalea-brigadier/tests/command_suggestions_test.rs index 8b137891..a907dd6e 100755 --- a/azalea-brigadier/tests/command_suggestions_test.rs +++ b/azalea-brigadier/tests/command_suggestions_test.rs @@ -1 +1,446 @@ +use azalea_brigadier::{ + context::StringRange, prelude::*, string_reader::StringReader, suggestion::Suggestion, +}; +fn test_suggestions( + subject: &CommandDispatcher<()>, + contents: &str, + cursor: usize, + range: StringRange, + suggestions: Vec<&str>, +) { + let result = CommandDispatcher::get_completion_suggestions_with_cursor( + subject.parse(contents.into(), ()), + cursor, + ); + assert_eq!(result.range(), range); + + let mut expected = Vec::new(); + for suggestion in suggestions { + expected.push(Suggestion::new(range, suggestion)); + } + + assert_eq!(result.list(), expected); +} + +fn input_with_offset(input: &str, offset: usize) -> StringReader { + let mut result = StringReader::from(input); + result.cursor = offset; + result +} + +#[test] +fn get_completion_suggestions_root_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("".into(), ())); + + assert_eq!(result.range(), StringRange::at(0)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(0), "bar"), + Suggestion::new(StringRange::at(0), "baz"), + Suggestion::new(StringRange::at(0), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse(input_with_offset("OOO", 3), ()), + ); + + assert_eq!(result.range(), StringRange::at(3)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(3), "bar"), + Suggestion::new(StringRange::at(3), "baz"), + Suggestion::new(StringRange::at(3), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_partial() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("b".into(), ())); + + assert_eq!(result.range(), StringRange::between(0, 1)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(0, 1), "bar"), + Suggestion::new(StringRange::between(0, 1), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_root_commands_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("baz")); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse(input_with_offset("Zb", 1), ()), + ); + + assert_eq!(result.range(), StringRange::between(1, 2)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(1, 2), "bar"), + Suggestion::new(StringRange::between(1, 2), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_sub_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let result = CommandDispatcher::get_completion_suggestions(subject.parse("parent ".into(), ())); + + assert_eq!(result.range(), StringRange::at(7)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(7), "bar"), + Suggestion::new(StringRange::at(7), "baz"), + Suggestion::new(StringRange::at(7), "foo") + ] + ); +} + +#[test] +fn get_completion_suggestions_moving_cursor_sub_commands() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")), + ); + + subject.register(literal("parent_two")); + + test_suggestions( + &subject, + "parent_one faz ", + 0, + StringRange::at(0), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 1, + StringRange::between(0, 1), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 7, + StringRange::between(0, 7), + vec!["parent_one", "parent_two"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 8, + StringRange::between(0, 8), + vec!["parent_one"], + ); + test_suggestions(&subject, "parent_one faz ", 10, StringRange::at(0), vec![]); + test_suggestions( + &subject, + "parent_one faz ", + 11, + StringRange::at(11), + vec!["faz", "fbz", "gaz"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 12, + StringRange::between(11, 12), + vec!["faz", "fbz"], + ); + test_suggestions( + &subject, + "parent_one faz ", + 13, + StringRange::between(11, 13), + vec!["faz"], + ); + test_suggestions(&subject, "parent_one faz ", 14, StringRange::at(0), vec![]); + test_suggestions(&subject, "parent_one faz ", 15, StringRange::at(0), vec![]); +} + +#[test] +fn get_completion_suggestions_sub_commands_partial() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let parse = subject.parse("parent b".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(7, 8)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(7, 8), "bar"), + Suggestion::new(StringRange::between(7, 8), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_sub_commands_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + subject.register( + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")), + ); + + let parse = subject.parse(input_with_offset("junk parent b", 5), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(12, 13)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::between(12, 13), "bar"), + Suggestion::new(StringRange::between(12, 13), "baz") + ] + ); +} + +#[test] +fn get_completion_suggestions_redirect() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse("redirect ".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::at(9)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::at(9), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_redirect_partial() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse("redirect s".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(9, 10)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(9, 10), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_moving_cursor_redirect() { + let mut subject = CommandDispatcher::<()>::new(); + let actual_one = subject.register( + literal("actual_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")), + ); + + subject.register(literal("actual_two")); + + subject.register(literal("redirect_one").redirect(actual_one.clone())); + subject.register(literal("redirect_two").redirect(actual_one)); + + test_suggestions( + &subject, + "redirect_one faz ", + 0, + StringRange::at(0), + vec!["actual_one", "actual_two", "redirect_one", "redirect_two"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 9, + StringRange::between(0, 9), + vec!["redirect_one", "redirect_two"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 10, + StringRange::between(0, 10), + vec!["redirect_one"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 12, + StringRange::at(0), + vec![], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 13, + StringRange::at(13), + vec!["faz", "fbz", "gaz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 14, + StringRange::between(13, 14), + vec!["faz", "fbz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 15, + StringRange::between(13, 15), + vec!["faz"], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 16, + StringRange::at(0), + vec![], + ); + test_suggestions( + &subject, + "redirect_one faz ", + 17, + StringRange::at(0), + vec![], + ); +} + +#[test] +fn get_completion_suggestions_redirect_partial_with_input_offset() { + let mut subject = CommandDispatcher::<()>::new(); + let actual = subject.register(literal("actual").then(literal("sub"))); + subject.register(literal("redirect").redirect(actual)); + + let parse = subject.parse(input_with_offset("/redirect s", 1), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::between(10, 11)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::between(10, 11), "sub")] + ); +} + +#[test] +fn get_completion_suggestions_redirect_lots() { + let mut subject = CommandDispatcher::<()>::new(); + let loop_ = subject.register(literal("redirect")); + subject.register( + literal("redirect").then(literal("loop").then(argument("loop", integer()).redirect(loop_))), + ); + + let result = CommandDispatcher::get_completion_suggestions( + subject.parse("redirect loop 1 loop 02 loop 003 ".into(), ()), + ); + + assert_eq!(result.range(), StringRange::at(33)); + assert_eq!( + result.list(), + vec![Suggestion::new(StringRange::at(33), "loop")] + ); +} + +#[test] +fn get_completion_suggestions_execute_simulation() { + let mut subject = CommandDispatcher::<()>::new(); + let execute = subject.register(literal("execute")); + subject.register( + literal("execute") + .then(literal("as").then(argument("name", word()).redirect(execute.clone()))) + .then(literal("store").then(argument("name", word()).redirect(execute))) + .then(literal("run").executes(|_| 0)), + ); + + let parse = subject.parse("execute as Dinnerbone as".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert!(result.is_empty()); +} + +#[test] +fn get_completion_suggestions_execute_simulation_partial() { + let mut subject = CommandDispatcher::<()>::new(); + let execute = subject.register(literal("execute")); + subject.register( + literal("execute") + .then( + literal("as") + .then(literal("bar").redirect(execute.clone())) + .then(literal("baz").redirect(execute.clone())), + ) + .then(literal("store").then(argument("name", word()).redirect(execute))) + .then(literal("run").executes(|_| 0)), + ); + + let parse = subject.parse("execute as bar as ".into(), ()); + + let result = CommandDispatcher::get_completion_suggestions(parse); + + assert_eq!(result.range(), StringRange::at(18)); + assert_eq!( + result.list(), + vec![ + Suggestion::new(StringRange::at(18), "bar"), + Suggestion::new(StringRange::at(18), "baz") + ] + ); +} diff --git a/azalea-brigadier/tests/context/command_context_test.rs b/azalea-brigadier/tests/context/command_context_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/context/command_context_test.rs +++ b/azalea-brigadier/tests/context/command_context_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/context/parsed_argument_test.rs b/azalea-brigadier/tests/context/parsed_argument_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/context/parsed_argument_test.rs +++ b/azalea-brigadier/tests/context/parsed_argument_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs b/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs +++ b/azalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs index e69de29b..8b137891 100755 --- a/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs +++ b/azalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs @@ -0,0 +1 @@ + diff --git a/azalea-brigadier/tests/suggestion/mod.rs b/azalea-brigadier/tests/suggestion/mod.rs index 7b96af2b..0ac50d52 100644 --- a/azalea-brigadier/tests/suggestion/mod.rs +++ b/azalea-brigadier/tests/suggestion/mod.rs @@ -1,3 +1,3 @@ mod suggestion_test; mod suggestions_builder_test; -mod suggestions_test; \ No newline at end of file +mod suggestions_test; diff --git a/azalea-brigadier/tests/suggestion/suggestions_test.rs b/azalea-brigadier/tests/suggestion/suggestions_test.rs index 12a2c73b..987dfb71 100755 --- a/azalea-brigadier/tests/suggestion/suggestions_test.rs +++ b/azalea-brigadier/tests/suggestion/suggestions_test.rs @@ -7,7 +7,7 @@ use azalea_brigadier::{ #[test] fn merge_empty() { - let merged = Suggestions::<()>::merge("foo b", &[]); + let merged = Suggestions::merge("foo b", &[]); assert!(merged.is_empty()); }