mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
brigadier usages
This commit is contained in:
parent
d5f424b8c2
commit
38db231ea8
28 changed files with 760 additions and 105 deletions
|
@ -16,6 +16,7 @@ pub enum ArgumentBuilderType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A node that hasn't yet been built.
|
/// A node that hasn't yet been built.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ArgumentBuilder<S> {
|
pub struct ArgumentBuilder<S> {
|
||||||
arguments: CommandNode<S>,
|
arguments: CommandNode<S>,
|
||||||
|
|
||||||
|
@ -134,6 +135,10 @@ impl<S> ArgumentBuilder<S> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn arguments(&self) -> &CommandNode<S> {
|
||||||
|
&self.arguments
|
||||||
|
}
|
||||||
|
|
||||||
/// Manually build this node into a [`CommandNode`]. You probably don't need
|
/// Manually build this node into a [`CommandNode`]. You probably don't need
|
||||||
/// to do this yourself.
|
/// to do this yourself.
|
||||||
pub fn build(self) -> CommandNode<S> {
|
pub fn build(self) -> CommandNode<S> {
|
||||||
|
|
|
@ -8,7 +8,13 @@ use crate::{
|
||||||
string_reader::StringReader,
|
string_reader::StringReader,
|
||||||
tree::CommandNode,
|
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.
|
/// 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 })
|
// 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> {
|
impl<S> Default for CommandDispatcher<S> {
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl<S> Clone for CommandContext<S> {
|
||||||
command: self.command.clone(),
|
command: self.command.clone(),
|
||||||
root_node: self.root_node.clone(),
|
root_node: self.root_node.clone(),
|
||||||
nodes: self.nodes.clone(),
|
nodes: self.nodes.clone(),
|
||||||
range: self.range.clone(),
|
range: self.range,
|
||||||
child: self.child.clone(),
|
child: self.child.clone(),
|
||||||
modifier: self.modifier.clone(),
|
modifier: self.modifier.clone(),
|
||||||
forks: self.forks,
|
forks: self.forks,
|
||||||
|
@ -67,7 +67,7 @@ impl<S> CommandContext<S> {
|
||||||
command: self.command.clone(),
|
command: self.command.clone(),
|
||||||
root_node: self.root_node.clone(),
|
root_node: self.root_node.clone(),
|
||||||
nodes: self.nodes.clone(),
|
nodes: self.nodes.clone(),
|
||||||
range: self.range.clone(),
|
range: self.range,
|
||||||
child: self.child.clone(),
|
child: self.child.clone(),
|
||||||
modifier: self.modifier.clone(),
|
modifier: self.modifier.clone(),
|
||||||
forks: self.forks,
|
forks: self.forks,
|
||||||
|
|
|
@ -34,7 +34,7 @@ impl<S> Clone for CommandContextBuilder<'_, S> {
|
||||||
source: self.source.clone(),
|
source: self.source.clone(),
|
||||||
command: self.command.clone(),
|
command: self.command.clone(),
|
||||||
child: self.child.clone(),
|
child: self.child.clone(),
|
||||||
range: self.range.clone(),
|
range: self.range,
|
||||||
modifier: self.modifier.clone(),
|
modifier: self.modifier.clone(),
|
||||||
forks: self.forks,
|
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 {
|
pub fn with_node(&mut self, node: Arc<RwLock<CommandNode<S>>>, range: StringRange) -> &Self {
|
||||||
self.nodes.push(ParsedCommandNode {
|
self.nodes.push(ParsedCommandNode {
|
||||||
node: node.clone(),
|
node: node.clone(),
|
||||||
range: range.clone(),
|
range,
|
||||||
});
|
});
|
||||||
self.range = StringRange::encompassing(&self.range, &range);
|
self.range = StringRange::encompassing(&self.range, &range);
|
||||||
self.modifier = node.read().modifier.clone();
|
self.modifier = node.read().modifier.clone();
|
||||||
|
@ -93,7 +93,7 @@ impl<'a, S> CommandContextBuilder<'a, S> {
|
||||||
source: self.source.clone(),
|
source: self.source.clone(),
|
||||||
command: self.command.clone(),
|
command: self.command.clone(),
|
||||||
child: self.child.clone().map(|c| Rc::new(c.build(input))),
|
child: self.child.clone().map(|c| Rc::new(c.build(input))),
|
||||||
range: self.range.clone(),
|
range: self.range,
|
||||||
forks: self.forks,
|
forks: self.forks,
|
||||||
modifier: self.modifier.clone(),
|
modifier: self.modifier.clone(),
|
||||||
input: input.to_string(),
|
input: input.to_string(),
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl<S> Clone for ParsedCommandNode<S> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
node: self.node.clone(),
|
node: self.node.clone(),
|
||||||
range: self.range.clone(),
|
range: self.range,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Copy)]
|
||||||
pub struct StringRange {
|
pub struct StringRange {
|
||||||
start: usize,
|
start: usize,
|
||||||
end: usize,
|
end: usize,
|
||||||
|
|
|
@ -8,6 +8,10 @@ use azalea_buf::McBufWritable;
|
||||||
use azalea_chat::FormattedText;
|
use azalea_chat::FormattedText;
|
||||||
#[cfg(feature = "azalea-buf")]
|
#[cfg(feature = "azalea-buf")]
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
hash::Hash,
|
||||||
|
};
|
||||||
pub use suggestions::Suggestions;
|
pub use suggestions::Suggestions;
|
||||||
pub use suggestions_builder::SuggestionsBuilder;
|
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
|
/// The `M` generic is the type of the tooltip, so for example a `String` or
|
||||||
/// just `()` if you don't care about it.
|
/// just `()` if you don't care about it.
|
||||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||||
pub struct Suggestion<M = String> {
|
pub struct Suggestion<M = ()>
|
||||||
pub text: String,
|
where
|
||||||
|
M: Clone,
|
||||||
|
{
|
||||||
pub range: StringRange,
|
pub range: StringRange,
|
||||||
|
value: SuggestionValue,
|
||||||
pub tooltip: Option<M>,
|
pub tooltip: Option<M>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Clone> Suggestion<M> {
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||||
pub fn apply(&self, input: &str) -> String {
|
pub enum SuggestionValue {
|
||||||
if self.range.start() == 0 && self.range.end() == input.len() {
|
Integer(i32),
|
||||||
return input.to_string();
|
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 {
|
if self.range.start() > 0 {
|
||||||
result.push_str(&input[0..self.range.start()]);
|
result.push_str(&input[0..self.range.start()]);
|
||||||
}
|
}
|
||||||
result.push_str(&self.text);
|
result.push_str(&text);
|
||||||
if self.range.end() < input.len() {
|
if self.range.end() < input.len() {
|
||||||
result.push_str(&input[self.range.end()..]);
|
result.push_str(&input[self.range.end()..]);
|
||||||
}
|
}
|
||||||
|
@ -39,30 +71,78 @@ impl<M: Clone> Suggestion<M> {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand(&self, command: &str, range: &StringRange) -> Suggestion<M> {
|
pub fn expand(&self, command: &str, range: StringRange) -> Suggestion<M> {
|
||||||
if range == &self.range {
|
if range == self.range {
|
||||||
return self.clone();
|
return self.clone();
|
||||||
}
|
}
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
if range.start() < self.range.start() {
|
if range.start() < self.range.start() {
|
||||||
result.push_str(&command[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() {
|
if range.end() > self.range.end() {
|
||||||
result.push_str(&command[self.range.end()..range.end()]);
|
result.push_str(&command[self.range.end()..range.end()]);
|
||||||
}
|
}
|
||||||
Suggestion {
|
Suggestion {
|
||||||
range: range.clone(),
|
range,
|
||||||
text: result,
|
value: SuggestionValue::Text(result),
|
||||||
tooltip: self.tooltip.clone(),
|
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")]
|
#[cfg(feature = "azalea-buf")]
|
||||||
impl McBufWritable for Suggestion<FormattedText> {
|
impl McBufWritable for Suggestion<FormattedText> {
|
||||||
fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
|
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)?;
|
self.tooltip.write_into(buf)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::Suggestion;
|
use super::Suggestion;
|
||||||
use crate::context::StringRange;
|
use crate::context::StringRange;
|
||||||
#[cfg(feature = "azalea-buf")]
|
#[cfg(feature = "azalea-buf")]
|
||||||
|
use crate::suggestion::SuggestionValue;
|
||||||
|
#[cfg(feature = "azalea-buf")]
|
||||||
use azalea_buf::{
|
use azalea_buf::{
|
||||||
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
|
BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable,
|
||||||
};
|
};
|
||||||
|
@ -11,12 +13,19 @@ use std::io::{Cursor, Write};
|
||||||
use std::{collections::HashSet, hash::Hash};
|
use std::{collections::HashSet, hash::Hash};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Suggestions<M = String> {
|
pub struct Suggestions<M>
|
||||||
pub range: StringRange,
|
where
|
||||||
pub suggestions: Vec<Suggestion<M>>,
|
M: Clone + PartialEq + Hash,
|
||||||
|
{
|
||||||
|
range: StringRange,
|
||||||
|
suggestions: Vec<Suggestion<M>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Clone + Eq + Hash> Suggestions<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 {
|
pub fn merge(command: &str, input: &[Suggestions<M>]) -> Self {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Suggestions::default();
|
return Suggestions::default();
|
||||||
|
@ -45,20 +54,34 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
|
||||||
let range = StringRange::new(start, end);
|
let range = StringRange::new(start, end);
|
||||||
let mut texts = HashSet::new();
|
let mut texts = HashSet::new();
|
||||||
for suggestion in suggestions {
|
for suggestion in suggestions {
|
||||||
texts.insert(suggestion.expand(command, &range));
|
texts.insert(suggestion.expand(command, range));
|
||||||
}
|
}
|
||||||
let mut sorted = texts.into_iter().collect::<Vec<_>>();
|
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 {
|
Suggestions {
|
||||||
range,
|
range,
|
||||||
suggestions: sorted,
|
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`
|
// this can't be derived because that'd require the generic to have `Default`
|
||||||
// too even if it's not actually necessary
|
// 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 {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
range: StringRange::default(),
|
range: StringRange::default(),
|
||||||
|
@ -85,12 +108,12 @@ impl McBufReadable for Suggestions<FormattedText> {
|
||||||
let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)?
|
let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| Suggestion {
|
.map(|s| Suggestion {
|
||||||
text: s.text,
|
value: SuggestionValue::Text(s.text),
|
||||||
tooltip: s.tooltip,
|
tooltip: s.tooltip,
|
||||||
range: range.clone(),
|
range: range.clone(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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 })
|
Ok(Suggestions { range, suggestions })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use crate::context::StringRange;
|
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: String,
|
||||||
input_lowercase: String,
|
input_lowercase: String,
|
||||||
start: usize,
|
start: usize,
|
||||||
remaining: String,
|
remaining: String,
|
||||||
remaining_lowercase: String,
|
remaining_lowercase: String,
|
||||||
result: HashSet<Suggestion>,
|
result: HashSet<Suggestion<M>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SuggestionsBuilder {
|
impl SuggestionsBuilder<()> {
|
||||||
pub fn new(input: &str, start: usize) -> Self {
|
pub fn new(input: &str, start: usize) -> Self {
|
||||||
Self::new_with_lowercase(input, input.to_lowercase().as_str(), start)
|
Self::new_with_lowercase(input, input.to_lowercase().as_str(), start)
|
||||||
}
|
}
|
||||||
|
@ -28,7 +33,9 @@ impl SuggestionsBuilder {
|
||||||
result: HashSet::new(),
|
result: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
|
||||||
pub fn input(&self) -> &str {
|
pub fn input(&self) -> &str {
|
||||||
&self.input
|
&self.input
|
||||||
}
|
}
|
||||||
|
@ -37,7 +44,7 @@ impl SuggestionsBuilder {
|
||||||
self.start
|
self.start
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remianing(&self) -> &str {
|
pub fn remaining(&self) -> &str {
|
||||||
&self.remaining
|
&self.remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +52,7 @@ impl SuggestionsBuilder {
|
||||||
&self.remaining_lowercase
|
&self.remaining_lowercase
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&self) -> Suggestions {
|
pub fn build(&self) -> Suggestions<M> {
|
||||||
Suggestions::create(&self.input, &self.result)
|
Suggestions::create(&self.input, &self.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,38 +62,53 @@ impl SuggestionsBuilder {
|
||||||
}
|
}
|
||||||
self.result.insert(Suggestion {
|
self.result.insert(Suggestion {
|
||||||
range: StringRange::between(self.start, self.input.len()),
|
range: StringRange::between(self.start, self.input.len()),
|
||||||
text: text.to_string(),
|
value: SuggestionValue::Text(text.to_string()),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
});
|
});
|
||||||
self
|
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 {
|
if text == self.remaining {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
self.result.insert(Suggestion {
|
self.result.insert(Suggestion {
|
||||||
range: StringRange::between(self.start, self.input.len()),
|
range: StringRange::between(self.start, self.input.len()),
|
||||||
text: text.to_string(),
|
value: SuggestionValue::Text(text.to_string()),
|
||||||
tooltip: Some(tooltip),
|
tooltip: Some(tooltip),
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: integer suggestions
|
pub fn suggest_integer(mut self, value: i32) -> Self {
|
||||||
// https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java#L74
|
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)]
|
#[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.result.extend(other.result);
|
||||||
self
|
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)
|
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)
|
self.create_offset(self.start)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,13 @@ use crate::{
|
||||||
modifier::RedirectModifier,
|
modifier::RedirectModifier,
|
||||||
string_reader::StringReader,
|
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>>;
|
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 struct CommandNode<S> {
|
||||||
pub value: ArgumentBuilderType,
|
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 literals: HashMap<String, Arc<RwLock<CommandNode<S>>>>,
|
||||||
pub arguments: 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>>>> {
|
pub fn child(&self, name: &str) -> Option<Arc<RwLock<CommandNode<S>>>> {
|
||||||
self.children.get(name).cloned()
|
self.children.get(name).cloned()
|
||||||
}
|
}
|
||||||
|
@ -216,7 +230,7 @@ impl<S> Default for CommandNode<S> {
|
||||||
Self {
|
Self {
|
||||||
value: ArgumentBuilderType::Literal(Literal::default()),
|
value: ArgumentBuilderType::Literal(Literal::default()),
|
||||||
|
|
||||||
children: HashMap::new(),
|
children: BTreeMap::new(),
|
||||||
literals: HashMap::new(),
|
literals: HashMap::new(),
|
||||||
arguments: HashMap::new(),
|
arguments: HashMap::new(),
|
||||||
|
|
||||||
|
|
6
azalea-brigadier/tests/arguments/mod.rs
Normal file
6
azalea-brigadier/tests/arguments/mod.rs
Normal 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;
|
|
@ -1,41 +1,17 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use azalea_brigadier::{builder::argument_builder::ArgumentBuilder, prelude::*};
|
||||||
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())); }
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_arguments() {
|
fn test_arguments() {
|
||||||
let mut builder: ArgumentBuilder<()> = literal("foo");
|
let builder: ArgumentBuilder<()> = literal("foo");
|
||||||
|
|
||||||
let argument: ArgumentBuilder<()> = argument("bar", integer());
|
let argument: ArgumentBuilder<()> = argument("bar", integer());
|
||||||
builder.then(argument.clone());
|
let builder = builder.then(argument.clone());
|
||||||
assert_eq!(builder.arguments.children.len(), 1);
|
assert_eq!(builder.arguments().children.len(), 1);
|
||||||
let built_argument = Rc::new(argument.build());
|
let built_argument = Rc::new(argument.build());
|
||||||
assert!(builder
|
assert!(builder
|
||||||
.arguments
|
.arguments()
|
||||||
.children
|
.children
|
||||||
.values()
|
.values()
|
||||||
.any(|e| *e.read() == *built_argument));
|
.any(|e| *e.read() == *built_argument));
|
||||||
|
|
3
azalea-brigadier/tests/builder/mod.rs
Normal file
3
azalea-brigadier/tests/builder/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod argument_builder_test;
|
||||||
|
mod literal_argument_builder_test;
|
||||||
|
mod required_argument_builder_test;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
2
azalea-brigadier/tests/context/mod.rs
Normal file
2
azalea-brigadier/tests/context/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod command_context_test;
|
||||||
|
mod parsed_argument_test;
|
2
azalea-brigadier/tests/exceptions/mod.rs
Normal file
2
azalea-brigadier/tests/exceptions/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod dynamic_command_syntax_exception_type_test;
|
||||||
|
mod simple_command_syntax_exception_type_test;
|
6
azalea-brigadier/tests/mod.rs
Normal file
6
azalea-brigadier/tests/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
mod arguments;
|
||||||
|
mod builder;
|
||||||
|
mod context;
|
||||||
|
mod exceptions;
|
||||||
|
mod suggestion;
|
||||||
|
mod tree;
|
3
azalea-brigadier/tests/suggestion/mod.rs
Normal file
3
azalea-brigadier/tests/suggestion/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod suggestion_test;
|
||||||
|
mod suggestions_builder_test;
|
||||||
|
mod suggestions_test;
|
|
@ -1,7 +1,12 @@
|
||||||
|
use azalea_brigadier::{context::StringRange, suggestion::Suggestion};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_insertation_start() {
|
fn apply_insertation_start() {
|
||||||
let suggestion = Suggestion::new(StringRange::at(0), "And so I said: ");
|
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]
|
#[test]
|
||||||
|
@ -49,23 +54,35 @@ fn expand_unchanged() {
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_left() {
|
fn expand_left() {
|
||||||
let suggestion = Suggestion::new(StringRange::at(1), "oo");
|
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]
|
#[test]
|
||||||
fn expand_right() {
|
fn expand_right() {
|
||||||
let suggestion = Suggestion::new(StringRange::at(0), "minecraft:");
|
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]
|
#[test]
|
||||||
fn expand_both() {
|
fn expand_both() {
|
||||||
let suggestion = Suggestion::new(StringRange::at(11), "minecraft:");
|
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]
|
#[test]
|
||||||
fn expand_replacement() {
|
fn expand_replacement() {
|
||||||
let suggestion = Suggestion::new(StringRange::between(6, 11), "strangers");
|
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!")
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,20 +1,58 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use azalea_brigadier::{
|
||||||
|
context::StringRange,
|
||||||
|
suggestion::{Suggestion, Suggestions},
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_empty() {
|
fn merge_empty() {
|
||||||
let merged = Suggestions::merge("foo b", vec![]);
|
let merged = Suggestions::<()>::merge("foo b", &[]);
|
||||||
assert!(merged.is_empty());
|
assert!(merged.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_single() {
|
fn merge_single() {
|
||||||
let suggestions = Suggestions::new(StringRange::at(5), vec![Suggestion::new(StringRange::at(5), "ar")]);
|
let suggestions = Suggestions::new(
|
||||||
let merged = Suggestions::merge("foo b", vec![suggestions]);
|
StringRange::at(5),
|
||||||
|
vec![Suggestion::new(StringRange::at(5), "ar")],
|
||||||
|
);
|
||||||
|
let merged = Suggestions::merge("foo b", &[suggestions.clone()]);
|
||||||
assert_eq!(merged, suggestions);
|
assert_eq!(merged, suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_multiple() {
|
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 a = Suggestions::new(
|
||||||
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")]);
|
StringRange::at(5),
|
||||||
let merged = Suggestions::merge("foo b", vec![a, b]);
|
vec![
|
||||||
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")]);
|
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);
|
||||||
}
|
}
|
1
azalea-brigadier/tests/tree/mod.rs
Normal file
1
azalea-brigadier/tests/tree/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{style::Style, FormattedText};
|
use crate::{style::Style, FormattedText};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
|
||||||
pub struct BaseComponent {
|
pub struct BaseComponent {
|
||||||
// implements mutablecomponent
|
// implements mutablecomponent
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
|
@ -15,7 +15,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A chat component, basically anything you can see in chat.
|
/// A chat component, basically anything you can see in chat.
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum FormattedText {
|
pub enum FormattedText {
|
||||||
Text(TextComponent),
|
Text(TextComponent),
|
||||||
|
|
|
@ -6,7 +6,7 @@ use once_cell::sync::Lazy;
|
||||||
use serde::{ser::SerializeStruct, Serialize, Serializer};
|
use serde::{ser::SerializeStruct, Serialize, Serializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
|
||||||
pub struct TextColor {
|
pub struct TextColor {
|
||||||
pub value: u32,
|
pub value: u32,
|
||||||
pub name: Option<String>,
|
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 {
|
pub struct Style {
|
||||||
// These are options instead of just bools because None is different than false in this case
|
// These are options instead of just bools because None is different than false in this case
|
||||||
pub color: Option<TextColor>,
|
pub color: Option<TextColor>,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSer
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
/// A component that contains text that's the same in all locales.
|
/// 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 struct TextComponent {
|
||||||
pub base: BaseComponent,
|
pub base: BaseComponent,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
|
use serde::{ser::SerializeMap, Serialize, Serializer, __private::ser::FlatMapSerializer};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum StringOrComponent {
|
pub enum StringOrComponent {
|
||||||
String(String),
|
String(String),
|
||||||
|
@ -13,7 +13,7 @@ pub enum StringOrComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A message whose content depends on the client's language.
|
/// A message whose content depends on the client's language.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct TranslatableComponent {
|
pub struct TranslatableComponent {
|
||||||
pub base: BaseComponent,
|
pub base: BaseComponent,
|
||||||
pub key: String,
|
pub key: String,
|
||||||
|
|
|
@ -19,14 +19,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_suggestions() {
|
fn test_suggestions() {
|
||||||
let suggestions = Suggestions {
|
let suggestions = Suggestions::new(
|
||||||
range: StringRange::new(0, 5),
|
StringRange::new(0, 5),
|
||||||
suggestions: vec![Suggestion {
|
vec![Suggestion::new_with_tooltip(
|
||||||
text: "foo".to_string(),
|
StringRange::new(1, 4),
|
||||||
range: StringRange::new(1, 4),
|
"foo",
|
||||||
tooltip: Some(FormattedText::from("bar".to_string())),
|
FormattedText::from("bar".to_string()),
|
||||||
}],
|
)],
|
||||||
};
|
);
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
suggestions.write_into(&mut buf).unwrap();
|
suggestions.write_into(&mut buf).unwrap();
|
||||||
let mut cursor = Cursor::new(&buf[..]);
|
let mut cursor = Cursor::new(&buf[..]);
|
||||||
|
|
Loading…
Add table
Reference in a new issue