diff options
| author | mat <git@matdoes.dev> | 2023-10-12 20:14:29 -0500 |
|---|---|---|
| committer | mat <git@matdoes.dev> | 2023-10-12 20:14:29 -0500 |
| commit | 38db231ea8fa0fb223e16637db0b6ec65b2b81ef (patch) | |
| tree | f8f61b04ad0b498b85bffb133c51af1670a33ddf /azalea-brigadier/src | |
| parent | d5f424b8c2fba9b3283aef36fe9e1e051636614c (diff) | |
| download | azalea-drasl-38db231ea8fa0fb223e16637db0b6ec65b2b81ef.tar.xz | |
brigadier usages
Diffstat (limited to 'azalea-brigadier/src')
| -rwxr-xr-x | azalea-brigadier/src/builder/argument_builder.rs | 5 | ||||
| -rwxr-xr-x | azalea-brigadier/src/command_dispatcher.rs | 184 | ||||
| -rwxr-xr-x | azalea-brigadier/src/context/command_context.rs | 4 | ||||
| -rwxr-xr-x | azalea-brigadier/src/context/command_context_builder.rs | 6 | ||||
| -rwxr-xr-x | azalea-brigadier/src/context/parsed_command_node.rs | 2 | ||||
| -rwxr-xr-x | azalea-brigadier/src/context/string_range.rs | 2 | ||||
| -rwxr-xr-x | azalea-brigadier/src/suggestion/mod.rs | 102 | ||||
| -rwxr-xr-x | azalea-brigadier/src/suggestion/suggestions.rs | 39 | ||||
| -rwxr-xr-x | azalea-brigadier/src/suggestion/suggestions_builder.rs | 50 | ||||
| -rwxr-xr-x | azalea-brigadier/src/tree/mod.rs | 20 |
10 files changed, 370 insertions, 44 deletions
diff --git a/azalea-brigadier/src/builder/argument_builder.rs b/azalea-brigadier/src/builder/argument_builder.rs index 643a3bd0..10191463 100755 --- a/azalea-brigadier/src/builder/argument_builder.rs +++ b/azalea-brigadier/src/builder/argument_builder.rs @@ -16,6 +16,7 @@ pub enum ArgumentBuilderType { } /// A node that hasn't yet been built. +#[derive(Clone)] pub struct ArgumentBuilder<S> { arguments: CommandNode<S>, @@ -134,6 +135,10 @@ impl<S> ArgumentBuilder<S> { self } + pub fn arguments(&self) -> &CommandNode<S> { + &self.arguments + } + /// Manually build this node into a [`CommandNode`]. You probably don't need /// to do this yourself. pub fn build(self) -> CommandNode<S> { diff --git a/azalea-brigadier/src/command_dispatcher.rs b/azalea-brigadier/src/command_dispatcher.rs index 672a250b..42737fba 100755 --- a/azalea-brigadier/src/command_dispatcher.rs +++ b/azalea-brigadier/src/command_dispatcher.rs @@ -8,7 +8,13 @@ use crate::{ string_reader::StringReader, tree::CommandNode, }; -use std::{cmp::Ordering, collections::HashMap, mem, rc::Rc, sync::Arc}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + mem, + rc::Rc, + sync::Arc, +}; /// The root of the command tree. You need to make this to register commands. /// @@ -297,6 +303,182 @@ impl<S> CommandDispatcher<S> { }) // Ok(if forked { successful_forks } else { result }) } + + pub fn get_all_usage( + &self, + node: &CommandNode<S>, + source: Arc<S>, + restricted: bool, + ) -> Vec<String> { + let mut result = vec![]; + self.get_all_usage_recursive(node, source, &mut result, "", restricted); + result + } + + fn get_all_usage_recursive( + &self, + node: &CommandNode<S>, + source: Arc<S>, + result: &mut Vec<String>, + prefix: &str, + restricted: bool, + ) { + if restricted && !node.can_use(source.clone()) { + return; + } + if node.command.is_some() { + result.push(prefix.to_owned()); + } + if let Some(redirect) = &node.redirect { + let redirect = if redirect.data_ptr() == self.root.data_ptr() { + "...".to_string() + } else { + format!("-> {}", redirect.read().usage_text()) + }; + if prefix.is_empty() { + result.push(format!("{} {redirect}", node.usage_text())); + } else { + result.push(format!("{prefix} {redirect}")); + } + } else { + for child in node.children.values() { + let child = child.read(); + self.get_all_usage_recursive( + &child, + Arc::clone(&source), + result, + if prefix.is_empty() { + child.usage_text() + } else { + format!("{prefix} {}", child.usage_text()) + } + .as_str(), + restricted, + ); + } + } + } + + /// Gets the possible executable commands from a specified node. + /// + /// You may use [`Self::root`] as a target to get usage data for the entire + /// command tree. + pub fn get_smart_usage( + &self, + node: &CommandNode<S>, + source: Arc<S>, + ) -> Vec<(Arc<RwLock<CommandNode<S>>>, String)> { + let mut result = Vec::new(); + + let optional = node.command.is_some(); + for child in node.children.values() { + let usage = + self.get_smart_usage_recursive(&child.read(), source.clone(), optional, false); + if let Some(usage) = usage { + result.push((child.clone(), usage)); + } + } + + result + } + + fn get_smart_usage_recursive( + &self, + node: &CommandNode<S>, + source: Arc<S>, + optional: bool, + deep: bool, + ) -> Option<String> { + if !node.can_use(source.clone()) { + return None; + } + + let this = if optional { + format!("[{}]", node.usage_text()) + } else { + node.usage_text() + }; + let child_optional = node.command.is_some(); + let open = if child_optional { "[" } else { "(" }; + let close = if child_optional { "]" } else { ")" }; + + if deep { + return Some(this); + } + + if let Some(redirect) = &node.redirect { + let redirect = if redirect.data_ptr() == self.root.data_ptr() { + "...".to_string() + } else { + format!("-> {}", redirect.read().usage_text()) + }; + return Some(format!("{this} {redirect}")); + } + + let children = node + .children + .values() + .filter(|child| child.read().can_use(source.clone())) + .collect::<Vec<_>>(); + match children.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + let usage = self.get_smart_usage_recursive( + &children[0].read(), + source.clone(), + child_optional, + child_optional, + ); + if let Some(usage) = usage { + return Some(format!("{this} {usage}")); + } + } + Ordering::Greater => { + let mut child_usage = HashSet::new(); + for child in &children { + let usage = self.get_smart_usage_recursive( + &child.read(), + source.clone(), + child_optional, + true, + ); + if let Some(usage) = usage { + child_usage.insert(usage); + } + } + match child_usage.len().cmp(&1) { + Ordering::Less => {} + Ordering::Equal => { + let usage = child_usage.into_iter().next().unwrap(); + let usage = if child_optional { + format!("[{}]", usage) + } else { + usage + }; + return Some(format!("{this} {usage}")); + } + Ordering::Greater => { + let mut builder = String::new(); + builder.push_str(open); + let mut count = 0; + for child in children { + if count > 0 { + builder.push('|'); + } + builder.push_str(&child.read().usage_text()); + count += 1; + } + if count > 0 { + builder.push_str(close); + return Some(format!("{this} {builder}")); + } + } + } + } + } + + Some(this) + } } impl<S> Default for CommandDispatcher<S> { diff --git a/azalea-brigadier/src/context/command_context.rs b/azalea-brigadier/src/context/command_context.rs index f78fe758..4d93006e 100755 --- a/azalea-brigadier/src/context/command_context.rs +++ b/azalea-brigadier/src/context/command_context.rs @@ -30,7 +30,7 @@ impl<S> Clone for CommandContext<S> { command: self.command.clone(), root_node: self.root_node.clone(), nodes: self.nodes.clone(), - range: self.range.clone(), + range: self.range, child: self.child.clone(), modifier: self.modifier.clone(), forks: self.forks, @@ -67,7 +67,7 @@ impl<S> CommandContext<S> { command: self.command.clone(), root_node: self.root_node.clone(), nodes: self.nodes.clone(), - range: self.range.clone(), + range: self.range, child: self.child.clone(), modifier: self.modifier.clone(), forks: self.forks, diff --git a/azalea-brigadier/src/context/command_context_builder.rs b/azalea-brigadier/src/context/command_context_builder.rs index 2fc8d4ac..99c40dac 100755 --- a/azalea-brigadier/src/context/command_context_builder.rs +++ b/azalea-brigadier/src/context/command_context_builder.rs @@ -34,7 +34,7 @@ impl<S> Clone for CommandContextBuilder<'_, S> { source: self.source.clone(), command: self.command.clone(), child: self.child.clone(), - range: self.range.clone(), + range: self.range, modifier: self.modifier.clone(), forks: self.forks, } @@ -77,7 +77,7 @@ impl<'a, S> CommandContextBuilder<'a, S> { pub fn with_node(&mut self, node: Arc<RwLock<CommandNode<S>>>, range: StringRange) -> &Self { self.nodes.push(ParsedCommandNode { node: node.clone(), - range: range.clone(), + range, }); self.range = StringRange::encompassing(&self.range, &range); self.modifier = node.read().modifier.clone(); @@ -93,7 +93,7 @@ impl<'a, S> CommandContextBuilder<'a, S> { source: self.source.clone(), command: self.command.clone(), child: self.child.clone().map(|c| Rc::new(c.build(input))), - range: self.range.clone(), + range: self.range, forks: self.forks, modifier: self.modifier.clone(), input: input.to_string(), diff --git a/azalea-brigadier/src/context/parsed_command_node.rs b/azalea-brigadier/src/context/parsed_command_node.rs index bba5d121..2d69c72e 100755 --- a/azalea-brigadier/src/context/parsed_command_node.rs +++ b/azalea-brigadier/src/context/parsed_command_node.rs @@ -14,7 +14,7 @@ impl<S> Clone for ParsedCommandNode<S> { fn clone(&self) -> Self { Self { node: self.node.clone(), - range: self.range.clone(), + range: self.range, } } } diff --git a/azalea-brigadier/src/context/string_range.rs b/azalea-brigadier/src/context/string_range.rs index 8ca88624..75163405 100755 --- a/azalea-brigadier/src/context/string_range.rs +++ b/azalea-brigadier/src/context/string_range.rs @@ -1,6 +1,6 @@ use std::cmp; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Copy)] pub struct StringRange { start: usize, end: usize, diff --git a/azalea-brigadier/src/suggestion/mod.rs b/azalea-brigadier/src/suggestion/mod.rs index c404adc7..bc0e7608 100755 --- a/azalea-brigadier/src/suggestion/mod.rs +++ b/azalea-brigadier/src/suggestion/mod.rs @@ -8,6 +8,10 @@ use azalea_buf::McBufWritable; use azalea_chat::FormattedText; #[cfg(feature = "azalea-buf")] use std::io::Write; +use std::{ + fmt::{self, Display}, + hash::Hash, +}; pub use suggestions::Suggestions; pub use suggestions_builder::SuggestionsBuilder; @@ -16,22 +20,50 @@ pub use suggestions_builder::SuggestionsBuilder; /// The `M` generic is the type of the tooltip, so for example a `String` or /// just `()` if you don't care about it. #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Suggestion<M = String> { - pub text: String, +pub struct Suggestion<M = ()> +where + M: Clone, +{ pub range: StringRange, + value: SuggestionValue, pub tooltip: Option<M>, } +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub enum SuggestionValue { + Integer(i32), + Text(String), +} + +impl Suggestion<()> { + pub fn new(range: StringRange, text: &str) -> Suggestion<()> { + Suggestion { + range, + value: SuggestionValue::Text(text.to_string()), + tooltip: None, + } + } +} + impl<M: Clone> Suggestion<M> { + pub fn new_with_tooltip(range: StringRange, text: &str, tooltip: M) -> Self { + Self { + range, + value: SuggestionValue::Text(text.to_string()), + tooltip: Some(tooltip), + } + } + pub fn apply(&self, input: &str) -> String { + let text = self.value.to_string(); if self.range.start() == 0 && self.range.end() == input.len() { - return input.to_string(); + return text; } - let mut result = String::with_capacity(self.text.len()); + let mut result = String::with_capacity(text.len()); if self.range.start() > 0 { result.push_str(&input[0..self.range.start()]); } - result.push_str(&self.text); + result.push_str(&text); if self.range.end() < input.len() { result.push_str(&input[self.range.end()..]); } @@ -39,30 +71,78 @@ impl<M: Clone> Suggestion<M> { result } - pub fn expand(&self, command: &str, range: &StringRange) -> Suggestion<M> { - if range == &self.range { + pub fn expand(&self, command: &str, range: StringRange) -> Suggestion<M> { + if range == self.range { return self.clone(); } let mut result = String::new(); if range.start() < self.range.start() { result.push_str(&command[range.start()..self.range.start()]); } - result.push_str(&self.text); + result.push_str(&self.value.to_string()); if range.end() > self.range.end() { result.push_str(&command[self.range.end()..range.end()]); } Suggestion { - range: range.clone(), - text: result, + range, + value: SuggestionValue::Text(result), tooltip: self.tooltip.clone(), } } + + pub fn text(&self) -> String { + self.value.to_string() + } +} + +impl SuggestionValue { + pub fn cmp_ignore_case(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (SuggestionValue::Text(a), SuggestionValue::Text(b)) => { + a.to_lowercase().cmp(&b.to_lowercase()) + } + (SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b), + _ => { + let a = self.to_string(); + let b = other.to_string(); + a.cmp(&b) + } + } + } +} + +impl Display for SuggestionValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SuggestionValue::Text(text) => write!(f, "{text}"), + SuggestionValue::Integer(value) => write!(f, "{value}"), + } + } +} + +impl Ord for SuggestionValue { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (SuggestionValue::Text(a), SuggestionValue::Text(b)) => a.cmp(b), + (SuggestionValue::Integer(a), SuggestionValue::Integer(b)) => a.cmp(b), + _ => { + let a = self.to_string(); + let b = other.to_string(); + a.cmp(&b) + } + } + } +} +impl PartialOrd for SuggestionValue { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } } #[cfg(feature = "azalea-buf")] impl McBufWritable for Suggestion<FormattedText> { fn write_into(&self, buf: &mut impl Write) -> Result<(), std::io::Error> { - self.text.write_into(buf)?; + self.value.to_string().write_into(buf)?; self.tooltip.write_into(buf)?; Ok(()) } diff --git a/azalea-brigadier/src/suggestion/suggestions.rs b/azalea-brigadier/src/suggestion/suggestions.rs index 2a8b5e9e..69877786 100755 --- a/azalea-brigadier/src/suggestion/suggestions.rs +++ b/azalea-brigadier/src/suggestion/suggestions.rs @@ -1,6 +1,8 @@ use super::Suggestion; use crate::context::StringRange; #[cfg(feature = "azalea-buf")] +use crate::suggestion::SuggestionValue; +#[cfg(feature = "azalea-buf")] use azalea_buf::{ BufReadError, McBuf, McBufReadable, McBufVarReadable, McBufVarWritable, McBufWritable, }; @@ -11,12 +13,19 @@ use std::io::{Cursor, Write}; use std::{collections::HashSet, hash::Hash}; #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Suggestions<M = String> { - pub range: StringRange, - pub suggestions: Vec<Suggestion<M>>, +pub struct Suggestions<M> +where + M: Clone + PartialEq + Hash, +{ + range: StringRange, + suggestions: Vec<Suggestion<M>>, } impl<M: Clone + Eq + Hash> Suggestions<M> { + pub fn new(range: StringRange, suggestions: Vec<Suggestion<M>>) -> Self { + Self { range, suggestions } + } + pub fn merge(command: &str, input: &[Suggestions<M>]) -> Self { if input.is_empty() { return Suggestions::default(); @@ -45,20 +54,34 @@ impl<M: Clone + Eq + Hash> Suggestions<M> { let range = StringRange::new(start, end); let mut texts = HashSet::new(); for suggestion in suggestions { - texts.insert(suggestion.expand(command, &range)); + texts.insert(suggestion.expand(command, range)); } let mut sorted = texts.into_iter().collect::<Vec<_>>(); - sorted.sort_by(|a, b| a.text.cmp(&b.text)); + + sorted.sort_by(|a, b| a.value.cmp_ignore_case(&b.value)); + Suggestions { range, suggestions: sorted, } } + + pub fn is_empty(&self) -> bool { + self.suggestions.is_empty() + } + + pub fn list(&self) -> &[Suggestion<M>] { + &self.suggestions + } + + pub fn range(&self) -> StringRange { + self.range + } } // this can't be derived because that'd require the generic to have `Default` // too even if it's not actually necessary -impl<M> Default for Suggestions<M> { +impl<M: Clone + Hash + Eq> Default for Suggestions<M> { fn default() -> Self { Self { range: StringRange::default(), @@ -85,12 +108,12 @@ impl McBufReadable for Suggestions<FormattedText> { let mut suggestions = Vec::<StandaloneSuggestion>::read_from(buf)? .into_iter() .map(|s| Suggestion { - text: s.text, + value: SuggestionValue::Text(s.text), tooltip: s.tooltip, range: range.clone(), }) .collect::<Vec<_>>(); - suggestions.sort_by(|a, b| a.text.cmp(&b.text)); + suggestions.sort_by(|a, b| a.value.cmp(&b.value)); Ok(Suggestions { range, suggestions }) } diff --git a/azalea-brigadier/src/suggestion/suggestions_builder.rs b/azalea-brigadier/src/suggestion/suggestions_builder.rs index 66f17fb1..469a7b98 100755 --- a/azalea-brigadier/src/suggestion/suggestions_builder.rs +++ b/azalea-brigadier/src/suggestion/suggestions_builder.rs @@ -1,19 +1,24 @@ use std::collections::HashSet; +use std::hash::Hash; use crate::context::StringRange; -use super::{Suggestion, Suggestions}; +use super::{Suggestion, SuggestionValue, Suggestions}; -pub struct SuggestionsBuilder { +#[derive(PartialEq, Debug)] +pub struct SuggestionsBuilder<M = ()> +where + M: Clone + Eq + Hash, +{ input: String, input_lowercase: String, start: usize, remaining: String, remaining_lowercase: String, - result: HashSet<Suggestion>, + result: HashSet<Suggestion<M>>, } -impl SuggestionsBuilder { +impl SuggestionsBuilder<()> { pub fn new(input: &str, start: usize) -> Self { Self::new_with_lowercase(input, input.to_lowercase().as_str(), start) } @@ -28,7 +33,9 @@ impl SuggestionsBuilder { result: HashSet::new(), } } +} +impl<M: Clone + Eq + Hash> SuggestionsBuilder<M> { pub fn input(&self) -> &str { &self.input } @@ -37,7 +44,7 @@ impl SuggestionsBuilder { self.start } - pub fn remianing(&self) -> &str { + pub fn remaining(&self) -> &str { &self.remaining } @@ -45,7 +52,7 @@ impl SuggestionsBuilder { &self.remaining_lowercase } - pub fn build(&self) -> Suggestions { + pub fn build(&self) -> Suggestions<M> { Suggestions::create(&self.input, &self.result) } @@ -55,38 +62,53 @@ impl SuggestionsBuilder { } self.result.insert(Suggestion { range: StringRange::between(self.start, self.input.len()), - text: text.to_string(), + value: SuggestionValue::Text(text.to_string()), tooltip: None, }); self } - pub fn suggest_with_tooltip(mut self, text: &str, tooltip: String) -> Self { + pub fn suggest_with_tooltip(mut self, text: &str, tooltip: M) -> Self { if text == self.remaining { return self; } self.result.insert(Suggestion { range: StringRange::between(self.start, self.input.len()), - text: text.to_string(), + value: SuggestionValue::Text(text.to_string()), tooltip: Some(tooltip), }); self } - // TODO: integer suggestions - // https://github.com/Mojang/brigadier/blob/master/src/main/java/com/mojang/brigadier/suggestion/SuggestionsBuilder.java#L74 + pub fn suggest_integer(mut self, value: i32) -> Self { + self.result.insert(Suggestion { + range: StringRange::between(self.start, self.input.len()), + value: SuggestionValue::Integer(value), + tooltip: None, + }); + self + } + + pub fn suggest_integer_with_tooltip(mut self, value: i32, tooltip: M) -> Self { + self.result.insert(Suggestion { + range: StringRange::between(self.start, self.input.len()), + value: SuggestionValue::Integer(value), + tooltip: Some(tooltip), + }); + self + } #[allow(clippy::should_implement_trait)] - pub fn add(mut self, other: SuggestionsBuilder) -> Self { + pub fn add(mut self, other: SuggestionsBuilder<M>) -> Self { self.result.extend(other.result); self } - pub fn create_offset(&self, start: usize) -> Self { + pub fn create_offset(&self, start: usize) -> SuggestionsBuilder<()> { SuggestionsBuilder::new_with_lowercase(&self.input, &self.input_lowercase, start) } - pub fn restart(self) -> Self { + pub fn restart(&self) -> SuggestionsBuilder<()> { self.create_offset(self.start) } } diff --git a/azalea-brigadier/src/tree/mod.rs b/azalea-brigadier/src/tree/mod.rs index cec972dc..8ab3526e 100755 --- a/azalea-brigadier/src/tree/mod.rs +++ b/azalea-brigadier/src/tree/mod.rs @@ -10,7 +10,13 @@ use crate::{ modifier::RedirectModifier, string_reader::StringReader, }; -use std::{collections::HashMap, fmt::Debug, hash::Hash, ptr, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::Debug, + hash::Hash, + ptr, + sync::Arc, +}; pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync>>; @@ -19,7 +25,8 @@ pub type Command<S> = Option<Arc<dyn Fn(&CommandContext<S>) -> i32 + Send + Sync pub struct CommandNode<S> { pub value: ArgumentBuilderType, - pub children: HashMap<String, Arc<RwLock<CommandNode<S>>>>, + // this is a BTreeMap because children need to be ordered when getting command suggestions + pub children: BTreeMap<String, Arc<RwLock<CommandNode<S>>>>, pub literals: HashMap<String, Arc<RwLock<CommandNode<S>>>>, pub arguments: HashMap<String, Arc<RwLock<CommandNode<S>>>>, @@ -125,6 +132,13 @@ impl<S> CommandNode<S> { } } + pub fn usage_text(&self) -> String { + match &self.value { + ArgumentBuilderType::Argument(argument) => format!("<{}>", argument.name), + ArgumentBuilderType::Literal(literal) => literal.value.to_owned(), + } + } + pub fn child(&self, name: &str) -> Option<Arc<RwLock<CommandNode<S>>>> { self.children.get(name).cloned() } @@ -216,7 +230,7 @@ impl<S> Default for CommandNode<S> { Self { value: ArgumentBuilderType::Literal(Literal::default()), - children: HashMap::new(), + children: BTreeMap::new(), literals: HashMap::new(), arguments: HashMap::new(), |
