brigadier usages

This commit is contained in:
mat 2023-10-12 20:14:29 -05:00
parent d5f424b8c2
commit 38db231ea8
28 changed files with 760 additions and 105 deletions

View file

@ -16,6 +16,7 @@ pub enum ArgumentBuilderType {
}
/// A node that hasn't yet been built.
#[derive(Clone)]
pub struct ArgumentBuilder<S> {
arguments: CommandNode<S>,
@ -134,6 +135,10 @@ impl<S> ArgumentBuilder<S> {
self
}
pub fn arguments(&self) -> &CommandNode<S> {
&self.arguments
}
/// Manually build this node into a [`CommandNode`]. You probably don't need
/// to do this yourself.
pub fn build(self) -> CommandNode<S> {

View file

@ -8,7 +8,13 @@ use crate::{
string_reader::StringReader,
tree::CommandNode,
};
use std::{cmp::Ordering, collections::HashMap, mem, rc::Rc, sync::Arc};
use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
mem,
rc::Rc,
sync::Arc,
};
/// The root of the command tree. You need to make this to register commands.
///
@ -297,6 +303,182 @@ impl<S> CommandDispatcher<S> {
})
// Ok(if forked { successful_forks } else { result })
}
pub fn get_all_usage(
&self,
node: &CommandNode<S>,
source: Arc<S>,
restricted: bool,
) -> Vec<String> {
let mut result = vec![];
self.get_all_usage_recursive(node, source, &mut result, "", restricted);
result
}
fn get_all_usage_recursive(
&self,
node: &CommandNode<S>,
source: Arc<S>,
result: &mut Vec<String>,
prefix: &str,
restricted: bool,
) {
if restricted && !node.can_use(source.clone()) {
return;
}
if node.command.is_some() {
result.push(prefix.to_owned());
}
if let Some(redirect) = &node.redirect {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
if prefix.is_empty() {
result.push(format!("{} {redirect}", node.usage_text()));
} else {
result.push(format!("{prefix} {redirect}"));
}
} else {
for child in node.children.values() {
let child = child.read();
self.get_all_usage_recursive(
&child,
Arc::clone(&source),
result,
if prefix.is_empty() {
child.usage_text()
} else {
format!("{prefix} {}", child.usage_text())
}
.as_str(),
restricted,
);
}
}
}
/// Gets the possible executable commands from a specified node.
///
/// You may use [`Self::root`] as a target to get usage data for the entire
/// command tree.
pub fn get_smart_usage(
&self,
node: &CommandNode<S>,
source: Arc<S>,
) -> Vec<(Arc<RwLock<CommandNode<S>>>, String)> {
let mut result = Vec::new();
let optional = node.command.is_some();
for child in node.children.values() {
let usage =
self.get_smart_usage_recursive(&child.read(), source.clone(), optional, false);
if let Some(usage) = usage {
result.push((child.clone(), usage));
}
}
result
}
fn get_smart_usage_recursive(
&self,
node: &CommandNode<S>,
source: Arc<S>,
optional: bool,
deep: bool,
) -> Option<String> {
if !node.can_use(source.clone()) {
return None;
}
let this = if optional {
format!("[{}]", node.usage_text())
} else {
node.usage_text()
};
let child_optional = node.command.is_some();
let open = if child_optional { "[" } else { "(" };
let close = if child_optional { "]" } else { ")" };
if deep {
return Some(this);
}
if let Some(redirect) = &node.redirect {
let redirect = if redirect.data_ptr() == self.root.data_ptr() {
"...".to_string()
} else {
format!("-> {}", redirect.read().usage_text())
};
return Some(format!("{this} {redirect}"));
}
let children = node
.children
.values()
.filter(|child| child.read().can_use(source.clone()))
.collect::<Vec<_>>();
match children.len().cmp(&1) {
Ordering::Less => {}
Ordering::Equal => {
let usage = self.get_smart_usage_recursive(
&children[0].read(),
source.clone(),
child_optional,
child_optional,
);
if let Some(usage) = usage {
return Some(format!("{this} {usage}"));
}
}
Ordering::Greater => {
let mut child_usage = HashSet::new();
for child in &children {
let usage = self.get_smart_usage_recursive(
&child.read(),
source.clone(),
child_optional,
true,
);
if let Some(usage) = usage {
child_usage.insert(usage);
}
}
match child_usage.len().cmp(&1) {
Ordering::Less => {}
Ordering::Equal => {
let usage = child_usage.into_iter().next().unwrap();
let usage = if child_optional {
format!("[{}]", usage)
} else {
usage
};
return Some(format!("{this} {usage}"));
}
Ordering::Greater => {
let mut builder = String::new();
builder.push_str(open);
let mut count = 0;
for child in children {
if count > 0 {
builder.push('|');
}
builder.push_str(&child.read().usage_text());
count += 1;
}
if count > 0 {
builder.push_str(close);
return Some(format!("{this} {builder}"));
}
}
}
}
}
Some(this)
}
}
impl<S> Default for CommandDispatcher<S> {

View file

@ -30,7 +30,7 @@ impl<S> Clone for CommandContext<S> {
command: self.command.clone(),
root_node: self.root_node.clone(),
nodes: self.nodes.clone(),
range: self.range.clone(),
range: self.range,
child: self.child.clone(),
modifier: self.modifier.clone(),
forks: self.forks,
@ -67,7 +67,7 @@ impl<S> CommandContext<S> {
command: self.command.clone(),
root_node: self.root_node.clone(),
nodes: self.nodes.clone(),
range: self.range.clone(),
range: self.range,
child: self.child.clone(),
modifier: self.modifier.clone(),
forks: self.forks,

View file

@ -34,7 +34,7 @@ impl<S> Clone for CommandContextBuilder<'_, S> {
source: self.source.clone(),
command: self.command.clone(),
child: self.child.clone(),
range: self.range.clone(),
range: self.range,
modifier: self.modifier.clone(),
forks: self.forks,
}
@ -77,7 +77,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
pub fn with_node(&mut self, node: Arc<RwLock<CommandNode<S>>>, range: StringRange) -> &Self {
self.nodes.push(ParsedCommandNode {
node: node.clone(),
range: range.clone(),
range,
});
self.range = StringRange::encompassing(&self.range, &range);
self.modifier = node.read().modifier.clone();
@ -93,7 +93,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
source: self.source.clone(),
command: self.command.clone(),
child: self.child.clone().map(|c| Rc::new(c.build(input))),
range: self.range.clone(),
range: self.range,
forks: self.forks,
modifier: self.modifier.clone(),
input: input.to_string(),

View file

@ -14,7 +14,7 @@ impl<S> Clone for ParsedCommandNode<S> {
fn clone(&self) -> Self {
Self {
node: self.node.clone(),
range: self.range.clone(),
range: self.range,
}
}
}

View file

@ -1,6 +1,6 @@
use std::cmp;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Copy)]
pub struct StringRange {
start: usize,
end: usize,

View file

@ -8,6 +8,10 @@ use azalea_buf::McBufWritable;
use azalea_chat::FormattedText;
#[cfg(feature = "azalea-buf")]
use std::io::Write;
use std::{
fmt::{self, Display},
hash::Hash,
};
pub use suggestions::Suggestions;
pub use suggestions_builder::SuggestionsBuilder;
@ -16,22 +20,50 @@ 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<M = String> {
pub text: String,
pub struct Suggestion<M = ()>
where
M: Clone,
{
pub range: StringRange,
value: SuggestionValue,
pub tooltip: Option<M>,
}
impl<M: Clone> Suggestion<M> {
pub fn apply(&self, input: &str) -> String {
if self.range.start() == 0 && self.range.end() == input.len() {
return input.to_string();
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum SuggestionValue {
Integer(i32),
Text(String),
}
impl Suggestion<()> {
pub fn new(range: StringRange, text: &str) -> Suggestion<()> {
Suggestion {
range,
value: SuggestionValue::Text(text.to_string()),
tooltip: None,
}
let mut result = String::with_capacity(self.text.len());
}
}
impl<M: Clone> Suggestion<M> {
pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: M) -> Self {
Self {
range,
value: SuggestionValue::Text(text.to_string()),
tooltip: Some(tooltip),
}
}
pub fn apply(&self, input: &str) -> String {
let text = self.value.to_string();
if self.range.start() == 0 && self.range.end() == input.len() {
return text;
}
let mut result = String::with_capacity(text.len());
if self.range.start() > 0 {
result.push_str(&input[0..self.range.start()]);
}
result.push_str(&self.text);
result.push_str(&text);
if self.range.end() < input.len() {
result.push_str(&input[self.range.end()..]);
}
@ -39,30 +71,78 @@ impl<M: Clone> Suggestion<M> {
result
}
pub fn expand(&self, command: &str, range: &StringRange) -> Suggestion<M> {
if range == &self.range {
pub fn expand(&self, command: &str, range: StringRange) -> Suggestion<M> {
if range == self.range {
return self.clone();
}
let mut result = String::new();
if range.start() < self.range.start() {
result.push_str(&command[range.start()..self.range.start()]);
}
result.push_str(&self.text);
result.push_str(&self.value.to_string());
if range.end() > self.range.end() {
result.push_str(&command[self.range.end()..range.end()]);
}
Suggestion {
range: range.clone(),
text: result,
range,
value: SuggestionValue::Text(result),
tooltip: self.tooltip.clone(),
}
}
pub fn text(&self) -> String {
self.value.to_string()
}
}
impl SuggestionValue {
pub fn cmp_ignore_case(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => {
a.to_lowercase().cmp(&b.to_lowercase())
}
(SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b),
_ => {
let a = self.to_string();
let b = other.to_string();
a.cmp(&b)
}
}
}
}
impl Display for SuggestionValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SuggestionValue::Text(text) => write!(f, "{text}"),
SuggestionValue::Integer(value) => write!(f, "{value}"),
}
}
}
impl Ord for SuggestionValue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(SuggestionValue::Text(a), SuggestionValue::Text(b)) => a.cmp(b),
(SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b),
_ => {
let a = self.to_string();
let b = other.to_string();
a.cmp(&b)
}
}
}
}
impl PartialOrd for SuggestionValue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "azalea-buf")]
impl McBufWritable for Suggestion<FormattedText> {
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.text.write_into(buf)?;
self.value.to_string().write_into(buf)?;
self.tooltip.write_into(buf)?;
Ok(())
}

View file

@ -1,6 +1,8 @@
use super::Suggestion;
use crate::context::StringRange;
#[cfg(feature = "azalea-buf")]
use crate::suggestion::SuggestionValue;
#[cfg(feature = "azalea-buf")]
use azalea_buf::{
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
};
@ -11,12 +13,19 @@ use std::io::{Cursor, Write};
use std::{collections::HashSet, hash::Hash};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Suggestions<M = String> {
pub range: StringRange,
pub suggestions: Vec<Suggestion<M>>,
pub struct Suggestions<M>
where
M: Clone + PartialEq + Hash,
{
range: StringRange,
suggestions: Vec<Suggestion<M>>,
}
impl<M: Clone + Eq + Hash> Suggestions<M> {
pub fn new(range: StringRange, suggestions: Vec<Suggestion<M>>) -> Self {
Self { range, suggestions }
}
pub fn merge(command: &str, input: &[Suggestions<M>]) -> Self {
if input.is_empty() {
return Suggestions::default();
@ -45,20 +54,34 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
let range = StringRange::new(start, end);
let mut texts = HashSet::new();
for suggestion in suggestions {
texts.insert(suggestion.expand(command, &range));
texts.insert(suggestion.expand(command, range));
}
let mut sorted = texts.into_iter().collect::<Vec<_>>();
sorted.sort_by(|a, b| a.text.cmp(&b.text));
sorted.sort_by(|a, b| a.value.cmp_ignore_case(&b.value));
Suggestions {
range,
suggestions: sorted,
}
}
pub fn is_empty(&self) -> bool {
self.suggestions.is_empty()
}
pub fn list(&self) -> &[Suggestion<M>] {
&self.suggestions
}
pub fn range(&self) -> StringRange {
self.range
}
}
// this can't be derived because that'd require the generic to have `Default`
// too even if it's not actually necessary
impl<M> Default for Suggestions<M> {
impl<M: Clone + Hash + Eq> Default for Suggestions<M> {
fn default() -> Self {
Self {
range: StringRange::default(),
@ -85,12 +108,12 @@ impl McBufReadable for Suggestions<FormattedText> {
let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)?
.into_iter()
.map(|s| Suggestion {
text: s.text,
value: SuggestionValue::Text(s.text),
tooltip: s.tooltip,
range: range.clone(),
})
.collect::<Vec<_>>();
suggestions.sort_by(|a, b| a.text.cmp(&b.text));
suggestions.sort_by(|a, b| a.value.cmp(&b.value));
Ok(Suggestions { range, suggestions })
}

View file

@ -1,19 +1,24 @@
use std::collections::HashSet;
use std::hash::Hash;
use crate::context::StringRange;
use super::{Suggestion, Suggestions};
use super::{Suggestion, SuggestionValue, Suggestions};
pub struct SuggestionsBuilder {
#[derive(PartialEq, Debug)]
pub struct SuggestionsBuilder<M = ()>
where
M: Clone + Eq + Hash,
{
input: String,
input_lowercase: String,
start: usize,
remaining: String,
remaining_lowercase: String,
result: HashSet<Suggestion>,
result: HashSet<Suggestion<M>>,
}
impl SuggestionsBuilder {
impl SuggestionsBuilder<()> {
pub fn new(input: &str, start: usize) -> Self {
Self::new_with_lowercase(input, input.to_lowercase().as_str(), start)
}
@ -28,7 +33,9 @@ impl SuggestionsBuilder {
result: HashSet::new(),
}
}
}
impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
pub fn input(&self) -> &str {
&self.input
}
@ -37,7 +44,7 @@ impl SuggestionsBuilder {
self.start
}
pub fn remianing(&self) -> &str {
pub fn remaining(&self) -> &str {
&self.remaining
}
@ -45,7 +52,7 @@ impl SuggestionsBuilder {
&self.remaining_lowercase
}
pub fn build(&self) -> Suggestions {
pub fn build(&self) -> Suggestions<M> {
Suggestions::create(&self.input, &self.result)
}
@ -55,38 +62,53 @@ impl SuggestionsBuilder {
}
self.result.insert(Suggestion {
range: StringRange::between(self.start, self.input.len()),
text: text.to_string(),
value: SuggestionValue::Text(text.to_string()),
tooltip: None,
});
self
}
pub fn suggest_with_tooltip(mut self, text: &str, tooltip: String) -> Self {
pub fn suggest_with_tooltip(mut self, text: &str, tooltip: M) -> Self {
if text == self.remaining {
return self;
}
self.result.insert(Suggestion {
range: StringRange::between(self.start, self.input.len()),
text: text.to_string(),
value: SuggestionValue::Text(text.to_string()),
tooltip: Some(tooltip),
});
self
}
// TODO: integer suggestions
// https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java#L74
pub fn suggest_integer(mut self, value: i32) -> Self {
self.result.insert(Suggestion {
range: StringRange::between(self.start, self.input.len()),
value: SuggestionValue::Integer(value),
tooltip: None,
});
self
}
pub fn suggest_integer_with_tooltip(mut self, value: i32, tooltip: M) -> Self {
self.result.insert(Suggestion {
range: StringRange::between(self.start, self.input.len()),
value: SuggestionValue::Integer(value),
tooltip: Some(tooltip),
});
self
}
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, other: SuggestionsBuilder) -> Self {
pub fn add(mut self, other: SuggestionsBuilder<M>) -> Self {
self.result.extend(other.result);
self
}
pub fn create_offset(&self, start: usize) -> Self {
pub fn create_offset(&self, start: usize) -> SuggestionsBuilder<()> {
SuggestionsBuilder::new_with_lowercase(&self.input, &self.input_lowercase, start)
}
pub fn restart(self) -> Self {
pub fn restart(&self) -> SuggestionsBuilder<()> {
self.create_offset(self.start)
}
}

View file

@ -10,7 +10,13 @@ use crate::{
modifier::RedirectModifier,
string_reader::StringReader,
};
use std::{collections::HashMap, fmt::Debug, hash::Hash, ptr, sync::Arc};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
hash::Hash,
ptr,
sync::Arc,
};
pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync>>;
@ -19,7 +25,8 @@ pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync
pub struct CommandNode<S> {
pub value: ArgumentBuilderType,
pub children: HashMap<String, Arc<RwLock<CommandNode<S>>>>,
// this is a BTreeMap because children need to be ordered when getting command suggestions
pub children: BTreeMap<String, Arc<RwLock<CommandNode<S>>>>,
pub literals: HashMap<String, Arc<RwLock<CommandNode<S>>>>,
pub arguments: HashMap<String, Arc<RwLock<CommandNode<S>>>>,
@ -125,6 +132,13 @@ impl<S> CommandNode<S> {
}
}
pub fn usage_text(&self) -> String {
match &self.value {
ArgumentBuilderType::Argument(argument) => format!("<{}>", argument.name),
ArgumentBuilderType::Literal(literal) => literal.value.to_owned(),
}
}
pub fn child(&self, name: &str) -> Option<Arc<RwLock<CommandNode<S>>>> {
self.children.get(name).cloned()
}
@ -216,7 +230,7 @@ impl<S> Default for CommandNode<S> {
Self {
value: ArgumentBuilderType::Literal(Literal::default()),
children: HashMap::new(),
children: BTreeMap::new(),
literals: HashMap::new(),
arguments: HashMap::new(),

View file

@ -0,0 +1,6 @@
mod bool_argument_type_test;
mod double_argument_type_test;
mod float_argument_type_test;
mod integer_argument_type_test;
mod long_argument_type_test;
mod string_argument_type_test;

View file

@ -1,41 +1,17 @@
use std::rc::Rc;
use crate::{
arguments::integer_argument_type::integer,
builder::{literal_argument_builder::literal, required_argument_builder::argument},
};
use super::ArgumentBuilder;
// public class ArgumentBuilderTest {
// private TestableArgumentBuilder<Object> builder;
// @Before
// public void setUp() throws Exception {
// builder = new TestableArgumentBuilder<>();
// }
// @Test
// public void testArguments() throws Exception {
// final RequiredArgumentBuilder<Object, ?> argument = argument("bar",
// integer());
// builder.then(argument);
// assertThat(builder.getArguments(), hasSize(1));
// assertThat(builder.getArguments(), hasItem((CommandNode<Object>)
// argument.build())); }
use azalea_brigadier::{builder::argument_builder::ArgumentBuilder, prelude::*};
#[test]
fn test_arguments() {
let mut builder: ArgumentBuilder<()> = literal("foo");
let builder: ArgumentBuilder<()> = literal("foo");
let argument: ArgumentBuilder<()> = argument("bar", integer());
builder.then(argument.clone());
assert_eq!(builder.arguments.children.len(), 1);
let builder = builder.then(argument.clone());
assert_eq!(builder.arguments().children.len(), 1);
let built_argument = Rc::new(argument.build());
assert!(builder
.arguments
.arguments()
.children
.values()
.any(|e| *e.read() == *built_argument));

View file

@ -0,0 +1,3 @@
mod argument_builder_test;
mod literal_argument_builder_test;
mod required_argument_builder_test;

View file

@ -1 +1,143 @@
use std::{collections::HashSet, sync::Arc};
use azalea_brigadier::{prelude::*, tree::CommandNode};
use parking_lot::RwLock;
fn setup() -> CommandDispatcher<()> {
let command = |_: &CommandContext<()>| 0;
let mut subject = CommandDispatcher::new();
subject.register(
literal("a")
.then(
literal("1")
.then(literal("i").executes(command))
.then(literal("ii").executes(command)),
)
.then(
literal("2")
.then(literal("i").executes(command))
.then(literal("ii").executes(command)),
),
);
subject.register(literal("b").then(literal("1").executes(command)));
subject.register(literal("c").executes(command));
subject.register(literal("d").requires(|_| false).executes(command));
subject.register(
literal("e").executes(command).then(
literal("1")
.executes(command)
.then(literal("i").executes(command))
.then(literal("ii").executes(command)),
),
);
subject.register(
literal("f")
.then(
literal("1")
.then(literal("i").executes(command))
.then(literal("ii").executes(command).requires(|_| false)),
)
.then(
literal("2")
.then(literal("i").executes(command).requires(|_| false))
.then(literal("ii").executes(command)),
),
);
subject.register(
literal("g")
.executes(command)
.then(literal("1").then(literal("i").executes(command))),
);
subject.register(
literal("h")
.executes(command)
.then(literal("1").then(literal("i").executes(command)))
.then(literal("2").then(literal("i").then(literal("ii").executes(command))))
.then(literal("3").executes(command)),
);
subject.register(
literal("i")
.executes(command)
.then(literal("1").executes(command))
.then(literal("2").executes(command)),
);
subject.register(literal("j").redirect(subject.root.clone()));
subject.register(literal("k").redirect(get(&subject, "h")));
subject
}
fn get(subject: &CommandDispatcher<()>, command: &str) -> Arc<RwLock<CommandNode<()>>> {
subject
.parse(command.into(), ())
.context
.nodes
.last()
.unwrap()
.node
.clone()
}
#[test]
fn test_all_usage_no_commands() {
let subject = CommandDispatcher::<()>::new();
let results = subject.get_all_usage(&subject.root.read(), Arc::new(()), true);
assert!(results.is_empty());
}
#[test]
fn test_smart_usage_no_commands() {
let subject = CommandDispatcher::<()>::new();
let results = subject.get_smart_usage(&subject.root.read(), Arc::new(()));
assert!(results.is_empty());
}
#[test]
fn test_all_usage_root() {
let subject = setup();
let results = subject.get_all_usage(&subject.root.read(), Arc::new(()), true);
let actual = results.into_iter().collect::<HashSet<_>>();
let expected = vec![
"a 1 i", "a 1 ii", "a 2 i", "a 2 ii", "b 1", "c", "e", "e 1", "e 1 i", "e 1 ii", "f 1 i",
"f 2 ii", "g", "g 1 i", "h", "h 1 i", "h 2 i ii", "h 3", "i", "i 1", "i 2", "j ...",
"k -> h",
]
.into_iter()
.map(|s| s.to_owned())
.collect::<HashSet<_>>();
assert_eq!(expected, actual);
}
#[test]
fn test_smart_usage_root() {
let subject = setup();
let results = subject.get_smart_usage(&subject.root.read(), Arc::new(()));
let actual = results
.into_iter()
.map(|(k, v)| (k.read().name().to_owned(), v))
.collect::<HashSet<_>>();
let expected = vec![
(get(&subject, "a"), "a (1|2)"),
(get(&subject, "b"), "b 1"),
(get(&subject, "c"), "c"),
(get(&subject, "e"), "e [1]"),
(get(&subject, "f"), "f (1|2)"),
(get(&subject, "g"), "g [1]"),
(get(&subject, "h"), "h [1|2|3]"),
(get(&subject, "i"), "i [1|2]"),
(get(&subject, "j"), "j ..."),
(get(&subject, "k"), "k -> h"),
];
println!("-");
let expected = expected
.into_iter()
.map(|(k, v)| (k.read().name().to_owned(), v.to_owned()))
.collect::<HashSet<_>>();
assert_eq!(actual, expected);
}

View file

@ -0,0 +1,2 @@
mod command_context_test;
mod parsed_argument_test;

View file

@ -0,0 +1,2 @@
mod dynamic_command_syntax_exception_type_test;
mod simple_command_syntax_exception_type_test;

View file

@ -0,0 +1,6 @@
mod arguments;
mod builder;
mod context;
mod exceptions;
mod suggestion;
mod tree;

View file

@ -0,0 +1,3 @@
mod suggestion_test;
mod suggestions_builder_test;
mod suggestions_test;

View file

@ -1,7 +1,12 @@
use azalea_brigadier::{context::StringRange, suggestion::Suggestion};
#[test]
fn apply_insertation_start() {
let suggestion = Suggestion::new(StringRange::at(0), "And so I said: ");
assert_eq!(suggestion.apply("Hello world!"), "And so I said: Hello world!");
assert_eq!(
suggestion.apply("Hello world!"),
"And so I said: Hello world!"
);
}
#[test]
@ -49,23 +54,35 @@ fn expand_unchanged() {
#[test]
fn expand_left() {
let suggestion = Suggestion::new(StringRange::at(1), "oo");
assert_eq!(suggestion.expand("f", StringRange::between(0, 1)), Suggestion::new(StringRange::between(0, 1), "foo"));
assert_eq!(
suggestion.expand("f", StringRange::between(0, 1)),
Suggestion::new(StringRange::between(0, 1), "foo")
);
}
#[test]
fn expand_right() {
let suggestion = Suggestion::new(StringRange::at(0), "minecraft:");
assert_eq!(suggestion.expand("fish", StringRange::between(0, 4)), Suggestion::new(StringRange::between(0, 4), "minecraft:fish"));
assert_eq!(
suggestion.expand("fish", StringRange::between(0, 4)),
Suggestion::new(StringRange::between(0, 4), "minecraft:fish")
);
}
#[test]
fn expand_both() {
let suggestion = Suggestion::new(StringRange::at(11), "minecraft:");
assert_eq!(suggestion.expand("give Steve fish_block", StringRange::between(5, 21)), Suggestion::new(StringRange::between(5, 21), "Steve minecraft:fish_block"));
assert_eq!(
suggestion.expand("give Steve fish_block", StringRange::between(5, 21)),
Suggestion::new(StringRange::between(5, 21), "Steve minecraft:fish_block")
);
}
#[test]
fn expand_replacement() {
let suggestion = Suggestion::new(StringRange::between(6, 11), "strangers");
assert_eq!(suggestion.expand("Hello world!", StringRange::between(0, 12)), Suggestion::new(StringRange::between(0, 12), "Hello strangers!"));
}
assert_eq!(
suggestion.expand("Hello world!", StringRange::between(0, 12)),
Suggestion::new(StringRange::between(0, 12), "Hello strangers!")
);
}

View file

@ -0,0 +1,133 @@
use std::collections::HashSet;
use azalea_brigadier::{
context::StringRange,
suggestion::{Suggestion, SuggestionsBuilder},
};
#[test]
fn suggest_appends() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder.suggest("orld!").build();
assert_eq!(
result.list(),
vec![Suggestion::new(StringRange::between(6, 7), "orld!")]
);
assert_eq!(result.range(), StringRange::between(6, 7));
assert!(!result.is_empty());
}
#[test]
fn suggest_replaces() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder.suggest("everybody").build();
assert_eq!(
result.list(),
vec![Suggestion::new(StringRange::between(6, 7), "everybody")]
);
assert_eq!(result.range(), StringRange::between(6, 7));
assert!(!result.is_empty());
}
#[test]
fn suggest_noop() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder.suggest("w").build();
assert_eq!(result.list(), vec![]);
assert!(result.is_empty());
}
#[test]
fn suggest_multiple() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder
.suggest("world!")
.suggest("everybody")
.suggest("weekend")
.build();
assert_eq!(
result.list(),
vec![
Suggestion::new(StringRange::between(6, 7), "everybody"),
Suggestion::new(StringRange::between(6, 7), "weekend"),
Suggestion::new(StringRange::between(6, 7), "world!"),
]
);
assert_eq!(result.range(), StringRange::between(6, 7));
assert!(!result.is_empty());
}
#[test]
fn restart() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let builder = builder.suggest("won't be included in restart");
let other = builder.restart();
assert_ne!(other, builder);
assert_eq!(other.input(), builder.input());
assert_eq!(other.start(), builder.start());
assert_eq!(other.remaining(), builder.remaining());
}
#[test]
fn sort_alphabetical() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder
.suggest("2")
.suggest("4")
.suggest("6")
.suggest("8")
.suggest("30")
.suggest("32")
.build();
let actual = result.list().iter().map(|s| s.text()).collect::<Vec<_>>();
assert_eq!(actual, vec!["2", "30", "32", "4", "6", "8"]);
}
#[test]
fn sort_numerical() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder
.suggest_integer(2)
.suggest_integer(4)
.suggest_integer(6)
.suggest_integer(8)
.suggest_integer(30)
.suggest_integer(32)
.build();
let actual = result.list().iter().map(|s| s.text()).collect::<Vec<_>>();
assert_eq!(actual, vec!["2", "4", "6", "8", "30", "32"]);
}
#[test]
fn sort_mixed() {
let builder = SuggestionsBuilder::new("Hello w", 6);
let result = builder
.suggest("11")
.suggest("22")
.suggest("33")
.suggest("a")
.suggest("b")
.suggest("c")
.suggest_integer(2)
.suggest_integer(4)
.suggest_integer(6)
.suggest_integer(8)
.suggest_integer(30)
.suggest_integer(32)
.suggest("3a")
.suggest("a3")
.build();
let actual = result
.list()
.iter()
.map(|s| s.text())
.collect::<HashSet<_>>();
// mojang please
let expected = vec![
"11", "2", "22", "33", "3a", "4", "6", "8", "30", "32", "a", "a3", "b", "c",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<_>>();
assert_eq!(actual, expected);
}

View file

@ -1,20 +1,58 @@
use std::collections::HashSet;
use azalea_brigadier::{
context::StringRange,
suggestion::{Suggestion, Suggestions},
};
#[test]
fn merge_empty() {
let merged = Suggestions::merge("foo b", vec![]);
let merged = Suggestions::<()>::merge("foo b", &[]);
assert!(merged.is_empty());
}
#[test]
fn merge_single() {
let suggestions = Suggestions::new(StringRange::at(5), vec![Suggestion::new(StringRange::at(5), "ar")]);
let merged = Suggestions::merge("foo b", vec![suggestions]);
let suggestions = Suggestions::new(
StringRange::at(5),
vec![Suggestion::new(StringRange::at(5), "ar")],
);
let merged = Suggestions::merge("foo b", &[suggestions.clone()]);
assert_eq!(merged, suggestions);
}
#[test]
fn merge_multiple() {
let a = Suggestions::new(StringRange::at(5), vec![Suggestion::new(StringRange::at(5), "ar"), Suggestion::new(StringRange::at(5), "az"), Suggestion::new(StringRange::at(5), "Az")]);
let b = Suggestions::new(StringRange::between(4, 5), vec![Suggestion::new(StringRange::between(4, 5), "foo"), Suggestion::new(StringRange::between(4, 5), "qux"), Suggestion::new(StringRange::between(4, 5), "apple"), Suggestion::new(StringRange::between(4, 5), "Bar")]);
let merged = Suggestions::merge("foo b", vec![a, b]);
assert_eq!(merged.get_list(), vec![Suggestion::new(StringRange::between(4, 5), "apple"), Suggestion::new(StringRange::between(4, 5), "ar"), Suggestion::new(StringRange::between(4, 5), "Az"), Suggestion::new(StringRange::between(4, 5), "bar"), Suggestion::new(StringRange::between(4, 5), "Bar"), Suggestion::new(StringRange::between(4, 5), "baz"), Suggestion::new(StringRange::between(4, 5), "bAz"), Suggestion::new(StringRange::between(4, 5), "foo"), Suggestion::new(StringRange::between(4, 5), "qux")]);
}
let a = Suggestions::new(
StringRange::at(5),
vec![
Suggestion::new(StringRange::at(5), "ar"),
Suggestion::new(StringRange::at(5), "az"),
Suggestion::new(StringRange::at(5), "Az"),
],
);
let b = Suggestions::new(
StringRange::between(4, 5),
vec![
Suggestion::new(StringRange::between(4, 5), "foo"),
Suggestion::new(StringRange::between(4, 5), "qux"),
Suggestion::new(StringRange::between(4, 5), "apple"),
Suggestion::new(StringRange::between(4, 5), "Bar"),
],
);
let merged = Suggestions::merge("foo b", &[a, b]);
let actual = merged.list().iter().cloned().collect::<HashSet<_>>();
let expected = vec![
Suggestion::new(StringRange::between(4, 5), "apple"),
Suggestion::new(StringRange::between(4, 5), "bar"),
Suggestion::new(StringRange::between(4, 5), "Bar"),
Suggestion::new(StringRange::between(4, 5), "baz"),
Suggestion::new(StringRange::between(4, 5), "bAz"),
Suggestion::new(StringRange::between(4, 5), "foo"),
Suggestion::new(StringRange::between(4, 5), "qux"),
]
.into_iter()
.collect::<HashSet<_>>();
assert_eq!(actual, expected);
}

View file

@ -0,0 +1 @@

View file

@ -1,7 +1,7 @@
use crate::{style::Style, FormattedText};
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
pub struct BaseComponent {
// implements mutablecomponent
#[serde(skip_serializing_if = "Vec::is_empty")]

View file

@ -15,7 +15,7 @@ use std::{
};
/// A chat component, basically anything you can see in chat.
#[derive(Clone, Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
#[serde(untagged)]
pub enum FormattedText {
Text(TextComponent),

View file

@ -6,7 +6,7 @@ use once_cell::sync::Lazy;
use serde::{ser::SerializeStruct, Serialize, Serializer};
use serde_json::Value;
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub struct TextColor {
pub value: u32,
pub name: Option<String>,
@ -290,7 +290,7 @@ impl TryFrom<ChatFormatting> for TextColor {
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Style {
// These are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>,

View file

@ -3,7 +3,7 @@ use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSer
use std::fmt::Display;
/// A component that contains text that's the same in all locales.
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct TextComponent {
pub base: BaseComponent,
pub text: String,

View file

@ -5,7 +5,7 @@ use crate::{
};
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
#[derive(Clone, Debug, PartialEq, Serialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
#[serde(untagged)]
pub enum StringOrComponent {
String(String),
@ -13,7 +13,7 @@ pub enum StringOrComponent {
}
/// A message whose content depends on the client's language.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TranslatableComponent {
pub base: BaseComponent,
pub key: String,

View file

@ -19,14 +19,14 @@ mod tests {
#[test]
fn test_suggestions() {
let suggestions = Suggestions {
range: StringRange::new(0, 5),
suggestions: vec![Suggestion {
text: "foo".to_string(),
range: StringRange::new(1, 4),
tooltip: Some(FormattedText::from("bar".to_string())),
}],
};
let suggestions = Suggestions::new(
StringRange::new(0, 5),
vec![Suggestion::new_with_tooltip(
StringRange::new(1, 4),
"foo",
FormattedText::from("bar".to_string()),
)],
);
let mut buf = Vec::new();
suggestions.write_into(&mut buf).unwrap();
let mut cursor = Cursor::new(&buf[..]);