aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2023-10-12 22:01:15 -0500
committermat <git@matdoes.dev>2023-10-12 22:01:15 -0500
commit79ad1e93bf6ce2b7c2da6925a7c85b33bb76f154 (patch)
tree19004abf51cb19eb3b5cc13c61d670889cb228a0
parentf505ace721d4c214cd32143febd0a066b6b95ce5 (diff)
downloadazalea-drasl-79ad1e93bf6ce2b7c2da6925a7c85b33bb76f154.tar.xz
brigadier suggestions
closes #109
-rwxr-xr-xazalea-brigadier/src/arguments/argument_type.rs14
-rw-r--r--azalea-brigadier/src/arguments/bool_argument_type.rs19
-rw-r--r--azalea-brigadier/src/arguments/double_argument_type.rs7
-rw-r--r--azalea-brigadier/src/arguments/float_argument_type.rs7
-rw-r--r--azalea-brigadier/src/arguments/integer_argument_type.rs7
-rw-r--r--azalea-brigadier/src/arguments/long_argument_type.rs7
-rw-r--r--azalea-brigadier/src/arguments/string_argument_type.rs11
-rwxr-xr-xazalea-brigadier/src/builder/required_argument_builder.rs16
-rwxr-xr-xazalea-brigadier/src/command_dispatcher.rs36
-rwxr-xr-xazalea-brigadier/src/context/command_context_builder.rs39
-rwxr-xr-xazalea-brigadier/src/context/mod.rs1
-rw-r--r--azalea-brigadier/src/context/suggestion_context.rs11
-rwxr-xr-xazalea-brigadier/src/suggestion/mod.rs24
-rwxr-xr-xazalea-brigadier/src/suggestion/suggestions.rs36
-rwxr-xr-xazalea-brigadier/src/suggestion/suggestions_builder.rs24
-rwxr-xr-xazalea-brigadier/src/tree/mod.rs24
-rwxr-xr-xazalea-brigadier/tests/arguments/bool_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/arguments/double_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/arguments/float_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/arguments/integer_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/arguments/long_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/arguments/string_argument_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/builder/literal_argument_builder_test.rs1
-rwxr-xr-xazalea-brigadier/tests/builder/required_argument_builder_test.rs1
-rwxr-xr-xazalea-brigadier/tests/command_dispatcher_usages_test.rs53
-rwxr-xr-xazalea-brigadier/tests/command_suggestions_test.rs445
-rwxr-xr-xazalea-brigadier/tests/context/command_context_test.rs1
-rwxr-xr-xazalea-brigadier/tests/context/parsed_argument_test.rs1
-rwxr-xr-xazalea-brigadier/tests/exceptions/dynamic_command_syntax_exception_type_test.rs1
-rwxr-xr-xazalea-brigadier/tests/exceptions/simple_command_syntax_exception_type_test.rs1
-rw-r--r--azalea-brigadier/tests/suggestion/mod.rs2
-rwxr-xr-xazalea-brigadier/tests/suggestion/suggestions_test.rs2
32 files changed, 738 insertions, 59 deletions
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<Arc<dyn Any>, CommandSyntaxException>;
+
+ fn list_suggestions(&self, _builder: SuggestionsBuilder) -> Suggestions {
+ Suggestions::default()
+ }
+
+ fn examples(&self) -> Vec<String> {
+ 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<Arc<dyn Any>, 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<String> {
+ 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<String> {
+ 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<String> {
+ 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<String> {
+ 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<String> {
+ 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<String> {
+ 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<Arc<dyn Any>, 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<String> {
+ self.parser.examples()
+ }
}
impl From<Argument> 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<S> CommandDispatcher<S> {
Some(this)
}
+
+ pub fn get_completion_suggestions(parse: ParseResults<S>) -> Suggestions {
+ let cursor = parse.reader.total_length();
+ Self::get_completion_suggestions_with_cursor(parse, cursor)
+ }
+
+ pub fn get_completion_suggestions_with_cursor(
+ parse: ParseResults<S>,
+ 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<S> Default for CommandDispatcher<S> {
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<S> {
+ 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<S> 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<S> {
+ pub parent: Arc<RwLock<CommandNode<S>>>,
+ 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<M = ()>
-where
- M: Clone,
-{
+pub struct Suggestion {
pub range: StringRange,
value: SuggestionValue,
- pub tooltip: Option<M>,
+ pub tooltip: Option<String>,
}
#[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<M: Clone> Suggestion<M> {
- 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<M: Clone> Suggestion<M> {
result
}
- pub fn expand(&self, command: &str, range: StringRange) -> Suggestion<M> {
+ 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<FormattedText> {
+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<M>
-where
- M: Clone + PartialEq + Hash,
-{
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
+pub struct Suggestions {
range: StringRange,
- suggestions: Vec<Suggestion<M>>,
+ suggestions: Vec<Suggestion>,
}
-impl<M: Clone + Eq + Hash> Suggestions<M> {
- pub fn new(range: StringRange, suggestions: Vec<Suggestion<M>>) -> Self {
+impl Suggestions {
+ pub fn new(range: StringRange, suggestions: Vec<Suggestion>) -> Self {
Self { range, suggestions }
}
- pub fn merge(command: &str, input: &[Suggestions<M>]) -> 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<M: Clone + Eq + Hash> Suggestions<M> {
Suggestions::create(command, &texts)
}
- pub fn create(command: &str, suggestions: &HashSet<Suggestion<M>>) -> Self {
+ pub fn create(command: &str, suggestions: &HashSet<Suggestion>) -> Self {
if suggestions.is_empty() {
return Suggestions::default();
};
@@ -70,7 +67,7 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
self.suggestions.is_empty()
}
- pub fn list(&self) -> &[Suggestion<M>] {
+ pub fn list(&self) -> &[Suggestion] {
&self.suggestions
}
@@ -79,19 +76,8 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
}
}
-// this can't be derived because that'd require the generic to have `Default`
-// too even if it's not actually necessary
-impl<M: Clone + Hash + Eq> Default for Suggestions<M> {
- fn default() -> Self {
- Self {
- range: StringRange::default(),
- suggestions: Vec::new(),
- }
- }
-}
-
#[cfg(feature = "azalea-buf")]
-impl McBufReadable for Suggestions<FormattedText> {
+impl McBufReadable for Suggestions {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
#[derive(McBuf)]
struct StandaloneSuggestion {
@@ -109,7 +95,7 @@ impl McBufReadable for Suggestions<FormattedText> {
.into_iter()
.map(|s| Suggestion {
value: SuggestionValue::Text(s.text),
- tooltip: s.tooltip,
+ tooltip: s.tooltip.map(|t| t.to_string()),
range,
})
.collect::<Vec<_>>();
@@ -120,7 +106,7 @@ impl McBufReadable for Suggestions<FormattedText> {
}
#[cfg(feature = "azalea-buf")]
-impl McBufWritable for Suggestions<FormattedText> {
+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<M = ()>
-where
- M: Clone + Eq + Hash,
-{
+pub struct SuggestionsBuilder {
input: String,
input_lowercase: String,
start: usize,
remaining: String,
remaining_lowercase: String,
- result: HashSet<Suggestion<M>>,
+ result: HashSet<Suggestion>,
}
-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<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
+impl SuggestionsBuilder {
pub fn input(&self) -> &str {
&self.input
}
@@ -52,7 +48,7 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
&self.remaining_lowercase
}
- pub fn build(&self) -> Suggestions<M> {
+ pub fn build(&self) -> Suggestions {
Suggestions::create(&self.input, &self.result)
}
@@ -68,7 +64,7 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
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<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
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<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
}
#[allow(clippy::should_implement_trait)]
- pub fn add(mut self, other: SuggestionsBuilder<M>) -> 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<S> CommandNode<S> {
}
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<S>,
+ 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<S> Debug for CommandNode<S> {
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::<HashSet<_>>();
+
+ 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::<HashSet<_>>();
+
+ 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::<HashSet<_>>();
+
+ 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::<HashSet<_>>();
+
+ 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());
}