1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

brigadier suggestions

closes #109
This commit is contained in:
mat 2023-10-12 22:01:15 -05:00
parent f505ace721
commit 79ad1e93bf
32 changed files with 738 additions and 59 deletions

View file

@ -1,7 +1,19 @@
use std::{any::Any, sync::Arc}; 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 { pub trait ArgumentType {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException>; 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![]
}
} }

View file

@ -1,7 +1,10 @@
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use crate::{ use crate::{
context::CommandContext, exceptions::CommandSyntaxException, string_reader::StringReader, context::CommandContext,
exceptions::CommandSyntaxException,
string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
}; };
use super::ArgumentType; use super::ArgumentType;
@ -13,6 +16,20 @@ impl ArgumentType for Boolean {
fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> { fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
Ok(Arc::new(reader.read_boolean()?)) 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 { pub fn bool() -> impl ArgumentType {

View file

@ -40,6 +40,13 @@ impl ArgumentType for Double {
} }
Ok(Arc::new(result)) 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 { pub fn double() -> impl ArgumentType {

View file

@ -40,6 +40,13 @@ impl ArgumentType for Float {
} }
Ok(Arc::new(result)) 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 { pub fn float() -> impl ArgumentType {

View file

@ -40,6 +40,13 @@ impl ArgumentType for Integer {
} }
Ok(Arc::new(result)) 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 { pub fn integer() -> impl ArgumentType {

View file

@ -40,6 +40,13 @@ impl ArgumentType for Long {
} }
Ok(Arc::new(result)) 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 { pub fn long() -> impl ArgumentType {

View file

@ -29,6 +29,17 @@ impl ArgumentType for StringArgument {
}; };
Ok(Arc::new(result)) 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. /// Match up until the next space.

View file

@ -1,6 +1,9 @@
use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType}; use super::argument_builder::{ArgumentBuilder, ArgumentBuilderType};
use crate::{ 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}; 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> { pub fn parse(&self, reader: &mut StringReader) -> Result<Arc<dyn Any>, CommandSyntaxException> {
self.parser.parse(reader) 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 { impl From<Argument> for ArgumentBuilderType {

View file

@ -6,6 +6,7 @@ use crate::{
exceptions::{BuiltInExceptions, CommandSyntaxException}, exceptions::{BuiltInExceptions, CommandSyntaxException},
parse_results::ParseResults, parse_results::ParseResults,
string_reader::StringReader, string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
tree::CommandNode, tree::CommandNode,
}; };
use std::{ use std::{
@ -474,6 +475,41 @@ impl<S> CommandDispatcher<S> {
Some(this) 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> { impl<S> Default for CommandDispatcher<S> {

View file

@ -2,7 +2,7 @@ use parking_lot::RwLock;
use super::{ use super::{
command_context::CommandContext, parsed_command_node::ParsedCommandNode, command_context::CommandContext, parsed_command_node::ParsedCommandNode,
string_range::StringRange, ParsedArgument, string_range::StringRange, suggestion_context::SuggestionContext, ParsedArgument,
}; };
use crate::{ use crate::{
command_dispatcher::CommandDispatcher, command_dispatcher::CommandDispatcher,
@ -99,6 +99,43 @@ impl<'a, S> CommandContextBuilder<'a, S> {
input: input.to_string(), 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> { impl<S> Debug for CommandContextBuilder<'_, S> {

View file

@ -3,6 +3,7 @@ mod command_context_builder;
mod parsed_argument; mod parsed_argument;
mod parsed_command_node; mod parsed_command_node;
mod string_range; mod string_range;
pub mod suggestion_context;
pub use command_context::CommandContext; pub use command_context::CommandContext;
pub use command_context_builder::CommandContextBuilder; pub use command_context_builder::CommandContextBuilder;

View file

@ -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,
}

View file

@ -20,13 +20,10 @@ 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 = ()> pub struct Suggestion {
where
M: Clone,
{
pub range: StringRange, pub range: StringRange,
value: SuggestionValue, value: SuggestionValue,
pub tooltip: Option<M>, pub tooltip: Option<String>,
} }
#[derive(Debug, Clone, Hash, Eq, PartialEq)] #[derive(Debug, Clone, Hash, Eq, PartialEq)]
@ -35,18 +32,16 @@ pub enum SuggestionValue {
Text(String), Text(String),
} }
impl Suggestion<()> { impl Suggestion {
pub fn new(range: StringRange, text: &str) -> Suggestion<()> { pub fn new(range: StringRange, text: &str) -> Suggestion {
Suggestion { Suggestion {
range, range,
value: SuggestionValue::Text(text.to_string()), value: SuggestionValue::Text(text.to_string()),
tooltip: None, tooltip: None,
} }
} }
}
impl<M: Clone> Suggestion<M> { pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: String) -> Self {
pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: M) -> Self {
Self { Self {
range, range,
value: SuggestionValue::Text(text.to_string()), value: SuggestionValue::Text(text.to_string()),
@ -71,7 +66,7 @@ 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 {
if range == self.range { if range == self.range {
return self.clone(); return self.clone();
} }
@ -140,10 +135,13 @@ impl PartialOrd for SuggestionValue {
} }
#[cfg(feature = "azalea-buf")] #[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> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
self.value.to_string().write_into(buf)?; self.value.to_string().write_into(buf)?;
self.tooltip.write_into(buf)?; self.tooltip
.clone()
.map(FormattedText::from)
.write_into(buf)?;
Ok(()) Ok(())
} }
} }

View file

@ -12,21 +12,18 @@ use azalea_chat::FormattedText;
use std::io::{Cursor, Write}; 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, Default)]
pub struct Suggestions<M> pub struct Suggestions {
where
M: Clone + PartialEq + Hash,
{
range: StringRange, range: StringRange,
suggestions: Vec<Suggestion<M>>, suggestions: Vec<Suggestion>,
} }
impl<M: Clone + Eq + Hash> Suggestions<M> { impl Suggestions {
pub fn new(range: StringRange, suggestions: Vec<Suggestion<M>>) -> Self { pub fn new(range: StringRange, suggestions: Vec<Suggestion>) -> Self {
Self { range, suggestions } Self { range, suggestions }
} }
pub fn merge(command: &str, input: &[Suggestions<M>]) -> Self { pub fn merge(command: &str, input: &[Suggestions]) -> Self {
if input.is_empty() { if input.is_empty() {
return Suggestions::default(); return Suggestions::default();
} else if input.len() == 1 { } else if input.len() == 1 {
@ -41,7 +38,7 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
Suggestions::create(command, &texts) 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() { if suggestions.is_empty() {
return Suggestions::default(); return Suggestions::default();
}; };
@ -70,7 +67,7 @@ impl<M: Clone + Eq + Hash> Suggestions<M> {
self.suggestions.is_empty() self.suggestions.is_empty()
} }
pub fn list(&self) -> &[Suggestion<M>] { pub fn list(&self) -> &[Suggestion] {
&self.suggestions &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")] #[cfg(feature = "azalea-buf")]
impl McBufReadable for Suggestions<FormattedText> { impl McBufReadable for Suggestions {
fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { fn read_from(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
#[derive(McBuf)] #[derive(McBuf)]
struct StandaloneSuggestion { struct StandaloneSuggestion {
@ -109,7 +95,7 @@ impl McBufReadable for Suggestions<FormattedText> {
.into_iter() .into_iter()
.map(|s| Suggestion { .map(|s| Suggestion {
value: SuggestionValue::Text(s.text), value: SuggestionValue::Text(s.text),
tooltip: s.tooltip, tooltip: s.tooltip.map(|t| t.to_string()),
range, range,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -120,7 +106,7 @@ impl McBufReadable for Suggestions<FormattedText> {
} }
#[cfg(feature = "azalea-buf")] #[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> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
(self.range.start() as u32).var_write_into(buf)?; (self.range.start() as u32).var_write_into(buf)?;
(self.range.length() as u32).var_write_into(buf)?; (self.range.length() as u32).var_write_into(buf)?;

View file

@ -1,24 +1,20 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash;
use crate::context::StringRange; use crate::context::StringRange;
use super::{Suggestion, SuggestionValue, Suggestions}; use super::{Suggestion, SuggestionValue, Suggestions};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct SuggestionsBuilder<M = ()> pub struct SuggestionsBuilder {
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<M>>, result: HashSet<Suggestion>,
} }
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)
} }
@ -35,7 +31,7 @@ impl SuggestionsBuilder<()> {
} }
} }
impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> { impl SuggestionsBuilder {
pub fn input(&self) -> &str { pub fn input(&self) -> &str {
&self.input &self.input
} }
@ -52,7 +48,7 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
&self.remaining_lowercase &self.remaining_lowercase
} }
pub fn build(&self) -> Suggestions<M> { pub fn build(&self) -> Suggestions {
Suggestions::create(&self.input, &self.result) Suggestions::create(&self.input, &self.result)
} }
@ -68,7 +64,7 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
self 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 { if text == self.remaining {
return self; return self;
} }
@ -89,7 +85,7 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
self 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 { self.result.insert(Suggestion {
range: StringRange::between(self.start, self.input.len()), range: StringRange::between(self.start, self.input.len()),
value: SuggestionValue::Integer(value), value: SuggestionValue::Integer(value),
@ -99,16 +95,16 @@ impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> {
} }
#[allow(clippy::should_implement_trait)] #[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.result.extend(other.result);
self 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) 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) self.create_offset(self.start)
} }
} }

View file

@ -9,6 +9,7 @@ use crate::{
exceptions::{BuiltInExceptions, CommandSyntaxException}, exceptions::{BuiltInExceptions, CommandSyntaxException},
modifier::RedirectModifier, modifier::RedirectModifier,
string_reader::StringReader, string_reader::StringReader,
suggestion::{Suggestions, SuggestionsBuilder},
}; };
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
@ -209,6 +210,29 @@ impl<S> CommandNode<S> {
} }
None 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> { impl<S> Debug for CommandNode<S> {

View file

@ -1,6 +1,6 @@
use std::{collections::HashSet, sync::Arc}; 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; use parking_lot::RwLock;
fn setup() -> CommandDispatcher<()> { fn setup() -> CommandDispatcher<()> {
@ -141,3 +141,54 @@ fn test_smart_usage_root() {
assert_eq!(actual, expected); 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);
}

View file

@ -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")
]
);
}

View file

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

View file

@ -7,7 +7,7 @@ use azalea_brigadier::{
#[test] #[test]
fn merge_empty() { fn merge_empty() {
let merged = Suggestions::<()>::merge("foo b", &[]); let merged = Suggestions::merge("foo b", &[]);
assert!(merged.is_empty()); assert!(merged.is_empty());
} }