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 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![]
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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.

View file

@ -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 {

View file

@ -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> {

View file

@ -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> {

View file

@ -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;

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
/// 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(())
}
}

View file

@ -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)?;

View file

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

View file

@ -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> {

View file

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

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

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