fix(completion): edge cases of operator completions (#15169)

# Description

Improves the completeness of operator completions.
Check the new test cases for details.

# User-Facing Changes

# Tests + Formatting

+4

# After Submitting
This commit is contained in:
zc he 2025-03-01 02:39:59 +08:00 committed by GitHub
parent c5a14bb8ff
commit 52a35827c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 989 additions and 637 deletions

1
Cargo.lock generated
View File

@ -3538,6 +3538,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"reedline", "reedline",
"rstest", "rstest",
"strum",
"sysinfo", "sysinfo",
"tempfile", "tempfile",
"unicode-segmentation", "unicode-segmentation",

View File

@ -155,6 +155,8 @@ serde_urlencoded = "0.7.1"
serde_yaml = "0.9.33" serde_yaml = "0.9.33"
sha2 = "0.10" sha2 = "0.10"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
strum = "0.26"
strum_macros = "0.26"
syn = "2.0" syn = "2.0"
sysinfo = "0.33" sysinfo = "0.33"
tabled = { version = "0.17.0", default-features = false } tabled = { version = "0.17.0", default-features = false }

View File

@ -40,6 +40,7 @@ miette = { workspace = true, features = ["fancy-no-backtrace"] }
nucleo-matcher = { workspace = true } nucleo-matcher = { workspace = true }
percent-encoding = { workspace = true } percent-encoding = { workspace = true }
sysinfo = { workspace = true } sysinfo = { workspace = true }
strum = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
uuid = { workspace = true, features = ["v4"] } uuid = { workspace = true, features = ["v4"] }
which = { workspace = true } which = { workspace = true }
@ -49,4 +50,4 @@ plugin = ["nu-plugin-engine"]
system-clipboard = ["reedline/system_clipboard"] system-clipboard = ["reedline/system_clipboard"]
[lints] [lints]
workspace = true workspace = true

View File

@ -31,6 +31,7 @@ pub enum SuggestionKind {
Command(nu_protocol::engine::CommandType), Command(nu_protocol::engine::CommandType),
Type(nu_protocol::Type), Type(nu_protocol::Type),
Module, Module,
Operator,
} }
impl From<Suggestion> for SemanticSuggestion { impl From<Suggestion> for SemanticSuggestion {

View File

@ -1,10 +1,10 @@
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
use nu_engine::{column::get_columns, eval_variable}; use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, FullCellPath, PathMember}, ast::{Expr, Expression, FullCellPath, PathMember},
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
eval_const::eval_constant, eval_const::eval_constant,
Span, Value, ShellError, Span, Value,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -42,37 +42,53 @@ impl Completer for CellPathCompletion<'_> {
}; };
let mut matcher = NuMatcher::new(prefix_str, options); let mut matcher = NuMatcher::new(prefix_str, options);
let value = eval_cell_path(
// evaluate the head expression to get its value working_set,
let value = if let Expr::Var(var_id) = self.full_cell_path.head.expr { stack,
working_set &self.full_cell_path.head,
.get_variable(var_id) path_members,
.const_val span,
.to_owned() )
.or_else(|| eval_variable(working_set.permanent_state, stack, var_id, span).ok())
} else {
eval_constant(working_set, &self.full_cell_path.head).ok()
}
.unwrap_or_default(); .unwrap_or_default();
for suggestion in nested_suggestions(&value, path_members, current_span) { for suggestion in get_suggestions_by_value(&value, current_span) {
matcher.add_semantic_suggestion(suggestion); matcher.add_semantic_suggestion(suggestion);
} }
matcher.results() matcher.results()
} }
} }
// Find recursively the values for cell_path /// Follow cell path to get the value
fn nested_suggestions( /// NOTE: This is a relatively lightweight implementation,
val: &Value, /// so it may fail to get the exact value when the expression is complicated.
/// One failing example would be `[$foo].0`
pub(crate) fn eval_cell_path(
working_set: &StateWorkingSet,
stack: &Stack,
head: &Expression,
path_members: &[PathMember], path_members: &[PathMember],
span: Span,
) -> Result<Value, ShellError> {
// evaluate the head expression to get its value
let head_value = if let Expr::Var(var_id) = head.expr {
working_set
.get_variable(var_id)
.const_val
.to_owned()
.map_or_else(
|| eval_variable(working_set.permanent_state, stack, var_id, span),
Ok,
)
} else {
eval_constant(working_set, head)
}?;
head_value.follow_cell_path(path_members, false)
}
fn get_suggestions_by_value(
value: &Value,
current_span: reedline::Span, current_span: reedline::Span,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
let value = val
.clone()
.follow_cell_path(path_members, false)
.unwrap_or_default();
let kind = SuggestionKind::Type(value.get_type()); let kind = SuggestionKind::Type(value.get_type());
let str_to_suggestion = |s: String| SemanticSuggestion { let str_to_suggestion = |s: String| SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {

View File

@ -2,155 +2,258 @@ use crate::completions::{
completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
}; };
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{self, Comparison, Expr, Expression},
engine::{Stack, StateWorkingSet}, engine::{Stack, StateWorkingSet},
Span, Type, Span, Type, Value, ENV_VARIABLE_ID,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use strum::{EnumMessage, IntoEnumIterator};
use super::cell_path_completions::eval_cell_path;
#[derive(Clone)] #[derive(Clone)]
pub struct OperatorCompletion<'a> { pub struct OperatorCompletion<'a> {
pub left_hand_side: &'a Expression, pub left_hand_side: &'a Expression,
} }
struct OperatorItem {
pub symbols: String,
pub description: String,
}
fn operator_to_item<T: EnumMessage + AsRef<str>>(op: T) -> OperatorItem {
OperatorItem {
symbols: op.as_ref().into(),
description: op.get_message().unwrap_or_default().into(),
}
}
fn common_comparison_ops() -> Vec<OperatorItem> {
vec![
operator_to_item(Comparison::In),
operator_to_item(Comparison::NotIn),
operator_to_item(Comparison::Equal),
operator_to_item(Comparison::NotEqual),
]
}
fn collection_comparison_ops() -> Vec<OperatorItem> {
let mut ops = common_comparison_ops();
ops.push(operator_to_item(Comparison::Has));
ops.push(operator_to_item(Comparison::NotHas));
ops
}
fn number_comparison_ops() -> Vec<OperatorItem> {
Comparison::iter()
.filter(|op| {
!matches!(
op,
Comparison::RegexMatch
| Comparison::NotRegexMatch
| Comparison::StartsWith
| Comparison::EndsWith
| Comparison::Has
| Comparison::NotHas
)
})
.map(operator_to_item)
.collect()
}
fn math_ops() -> Vec<OperatorItem> {
ast::Math::iter()
.filter(|op| !matches!(op, ast::Math::Concatenate | ast::Math::Pow))
.map(operator_to_item)
.collect()
}
fn bit_ops() -> Vec<OperatorItem> {
ast::Bits::iter().map(operator_to_item).collect()
}
fn numeric_assignment_ops() -> Vec<OperatorItem> {
ast::Assignment::iter()
.filter(|op| !matches!(op, ast::Assignment::ConcatenateAssign))
.map(operator_to_item)
.collect()
}
fn concat_assignment_ops() -> Vec<OperatorItem> {
vec![
operator_to_item(ast::Assignment::Assign),
operator_to_item(ast::Assignment::ConcatenateAssign),
]
}
fn valid_int_ops() -> Vec<OperatorItem> {
let mut ops = valid_float_ops();
ops.extend(bit_ops());
ops
}
fn valid_float_ops() -> Vec<OperatorItem> {
let mut ops = valid_value_with_unit_ops();
ops.push(operator_to_item(ast::Math::Pow));
ops
}
fn valid_string_ops() -> Vec<OperatorItem> {
let mut ops: Vec<OperatorItem> = Comparison::iter().map(operator_to_item).collect();
ops.push(operator_to_item(ast::Math::Concatenate));
ops.push(OperatorItem {
symbols: "like".into(),
description: Comparison::RegexMatch
.get_message()
.unwrap_or_default()
.into(),
});
ops.push(OperatorItem {
symbols: "not-like".into(),
description: Comparison::NotRegexMatch
.get_message()
.unwrap_or_default()
.into(),
});
ops
}
fn valid_list_ops() -> Vec<OperatorItem> {
let mut ops = collection_comparison_ops();
ops.push(operator_to_item(ast::Math::Concatenate));
ops
}
fn valid_binary_ops() -> Vec<OperatorItem> {
let mut ops = number_comparison_ops();
ops.extend(bit_ops());
ops.push(operator_to_item(ast::Math::Concatenate));
ops
}
fn valid_bool_ops() -> Vec<OperatorItem> {
let mut ops: Vec<OperatorItem> = ast::Boolean::iter().map(operator_to_item).collect();
ops.extend(common_comparison_ops());
ops
}
fn valid_value_with_unit_ops() -> Vec<OperatorItem> {
let mut ops = number_comparison_ops();
ops.extend(math_ops());
ops
}
fn ops_by_value(value: &Value, mutable: bool) -> Vec<OperatorItem> {
let mut ops = match value {
Value::Int { .. } => valid_int_ops(),
Value::Float { .. } => valid_float_ops(),
Value::String { .. } => valid_string_ops(),
Value::Binary { .. } => valid_binary_ops(),
Value::Bool { .. } => valid_bool_ops(),
Value::Date { .. } => number_comparison_ops(),
Value::Filesize { .. } | Value::Duration { .. } => valid_value_with_unit_ops(),
Value::Range { .. } | Value::Record { .. } => collection_comparison_ops(),
Value::List { .. } => valid_list_ops(),
_ => common_comparison_ops(),
};
if mutable {
ops.extend(match value {
Value::Int { .. }
| Value::Float { .. }
| Value::Filesize { .. }
| Value::Duration { .. } => numeric_assignment_ops(),
Value::String { .. } | Value::Binary { .. } | Value::List { .. } => {
concat_assignment_ops()
}
_ => vec![operator_to_item(ast::Assignment::Assign)],
})
}
ops
}
fn is_expression_mutable(expr: &Expr, working_set: &StateWorkingSet) -> bool {
let Expr::FullCellPath(path) = expr else {
return false;
};
let Expr::Var(id) = path.head.expr else {
return false;
};
if id == ENV_VARIABLE_ID {
return true;
}
let var = working_set.get_variable(id);
var.mutable
}
impl Completer for OperatorCompletion<'_> { impl Completer for OperatorCompletion<'_> {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack, stack: &Stack,
prefix: impl AsRef<str>, prefix: impl AsRef<str>,
span: Span, span: Span,
offset: usize, offset: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<SemanticSuggestion> {
//Check if int, float, or string let mut needs_assignment_ops = true;
let possible_operations = match &self.left_hand_side.expr { // Complete according expression type
Expr::Int(_) => vec![ // TODO: type inference on self.left_hand_side to get more accurate completions
("+", "Add (Plus)"), let mut possible_operations: Vec<OperatorItem> = match &self.left_hand_side.ty {
("-", "Subtract (Minus)"), Type::Int | Type::Number => valid_int_ops(),
("*", "Multiply"), Type::Float => valid_float_ops(),
("/", "Divide"), Type::String => valid_string_ops(),
("==", "Equal to"), Type::Binary => valid_binary_ops(),
("!=", "Not equal to"), Type::Bool => valid_bool_ops(),
("//", "Floor division"), Type::Date => number_comparison_ops(),
("<", "Less than"), Type::Filesize | Type::Duration => valid_value_with_unit_ops(),
(">", "Greater than"), Type::Record(_) | Type::Range => collection_comparison_ops(),
("<=", "Less than or equal to"), Type::List(_) | Type::Table(_) => valid_list_ops(),
(">=", "Greater than or equal to"), // Unknown type, resort to evaluated values
("mod", "Floor division remainder (Modulo)"), Type::Any => match &self.left_hand_side.expr {
("**", "Power of"), Expr::FullCellPath(path) => {
("bit-or", "Bitwise OR"), // for `$ <tab>`
("bit-xor", "Bitwise exclusive OR"), if matches!(path.head.expr, Expr::Garbage) {
("bit-and", "Bitwise AND"), return vec![];
("bit-shl", "Bitwise shift left"), }
("bit-shr", "Bitwise shift right"), let value =
("in", "Is a member of (doesn't use regex)"), eval_cell_path(working_set, stack, &path.head, &path.tail, path.head.span)
("not-in", "Is not a member of (doesn't use regex)"), .unwrap_or_default();
], let mutable = is_expression_mutable(&self.left_hand_side.expr, working_set);
Expr::String(_) => vec![ // to avoid duplication
("=~", "Contains regex match"), needs_assignment_ops = false;
("like", "Contains regex match"), ops_by_value(&value, mutable)
("!~", "Does not contain regex match"), }
("not-like", "Does not contain regex match"), _ => common_comparison_ops(),
(
"++",
"Concatenates two lists, two strings, or two binary values",
),
("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"),
("starts-with", "Starts with"),
("ends-with", "Ends with"),
],
Expr::Float(_) => vec![
("+", "Add (Plus)"),
("-", "Subtract (Minus)"),
("*", "Multiply"),
("/", "Divide"),
("==", "Equal to"),
("!=", "Not equal to"),
("//", "Floor division"),
("<", "Less than"),
(">", "Greater than"),
("<=", "Less than or equal to"),
(">=", "Greater than or equal to"),
("mod", "Floor division remainder (Modulo)"),
("**", "Power of"),
("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"),
],
Expr::Bool(_) => vec![
(
"and",
"Both values are true (short-circuits when first value is false)",
),
(
"or",
"Either value is true (short-circuits when first value is true)",
),
("xor", "One value is true and the other is false"),
("not", "Negates a value or expression"),
("in", "Is a member of (doesn't use regex)"),
("not-in", "Is not a member of (doesn't use regex)"),
],
Expr::FullCellPath(path) => match path.head.expr {
Expr::List(_) => vec![
(
"++",
"Concatenates two lists, two strings, or two binary values",
),
("has", "Contains a value of (doesn't use regex)"),
("not-has", "Does not contain a value of (doesn't use regex)"),
],
Expr::Var(id) => get_variable_completions(id, working_set),
_ => vec![],
}, },
_ => vec![], _ => common_comparison_ops(),
}; };
// If the left hand side is a variable, add assignment operators if mutable
if needs_assignment_ops && is_expression_mutable(&self.left_hand_side.expr, working_set) {
possible_operations.extend(match &self.left_hand_side.ty {
Type::Int | Type::Float | Type::Number => numeric_assignment_ops(),
Type::Filesize | Type::Duration => numeric_assignment_ops(),
Type::String | Type::Binary | Type::List(_) => concat_assignment_ops(),
_ => vec![operator_to_item(ast::Assignment::Assign)],
});
}
let mut matcher = NuMatcher::new(prefix, options); let mut matcher = NuMatcher::new(prefix, options);
for (symbol, desc) in possible_operations.into_iter() { for OperatorItem {
symbols,
description,
} in possible_operations
{
matcher.add_semantic_suggestion(SemanticSuggestion { matcher.add_semantic_suggestion(SemanticSuggestion {
suggestion: Suggestion { suggestion: Suggestion {
value: symbol.to_string(), value: symbols.to_owned(),
description: Some(desc.to_string()), description: Some(description.to_owned()),
span: reedline::Span::new(span.start - offset, span.end - offset), span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true, append_whitespace: true,
..Suggestion::default() ..Suggestion::default()
}, },
kind: Some(SuggestionKind::Command( kind: Some(SuggestionKind::Operator),
nu_protocol::engine::CommandType::Builtin,
)),
}); });
} }
matcher.results() matcher.results()
} }
} }
pub fn get_variable_completions<'a>(
id: nu_protocol::Id<nu_protocol::marker::Var>,
working_set: &StateWorkingSet,
) -> Vec<(&'a str, &'a str)> {
let var = working_set.get_variable(id);
if !var.mutable {
return vec![];
}
match var.ty {
Type::List(_) | Type::String | Type::Binary => vec![
(
"++=",
"Concatenates two lists, two strings, or two binary values",
),
("=", "Assigns a value to a variable."),
],
Type::Int | Type::Float => vec![
("=", "Assigns a value to a variable."),
("+=", "Adds a value to a variable."),
("-=", "Subtracts a value from a variable."),
("*=", "Multiplies a variable by a value"),
("/=", "Divides a variable by a value."),
],
_ => vec![],
}
}

File diff suppressed because it is too large Load Diff

View File

@ -217,7 +217,7 @@ pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
} }
/// match a list of suggestions with the expected values /// match a list of suggestions with the expected values
pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>) { pub fn match_suggestions(expected: &Vec<&str>, suggestions: &Vec<Suggestion>) {
let expected_len = expected.len(); let expected_len = expected.len();
let suggestions_len = suggestions.len(); let suggestions_len = suggestions.len();
if expected_len != suggestions_len { if expected_len != suggestions_len {
@ -230,12 +230,18 @@ pub fn match_suggestions(expected: &Vec<String>, suggestions: &Vec<Suggestion>)
let suggestions_str = suggestions let suggestions_str = suggestions
.iter() .iter()
.map(|it| it.value.clone()) .map(|it| it.value.as_str())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(expected, &suggestions_str); assert_eq!(expected, &suggestions_str);
} }
/// match a list of suggestions with the expected values
pub fn match_suggestions_by_string(expected: &[String], suggestions: &Vec<Suggestion>) {
let expected = expected.iter().map(|it| it.as_str()).collect::<Vec<_>>();
match_suggestions(&expected, suggestions);
}
/// append the separator to the converted path /// append the separator to the converted path
pub fn folder(path: impl Into<PathBuf>) -> String { pub fn folder(path: impl Into<PathBuf>) -> String {
let mut converted_path = file(path); let mut converted_path = file(path);

View File

@ -1,3 +1,5 @@
pub mod completions_helpers; pub mod completions_helpers;
pub use completions_helpers::{file, folder, match_suggestions, merge_input, new_engine}; pub use completions_helpers::{
file, folder, match_suggestions, match_suggestions_by_string, merge_input, new_engine,
};

View File

@ -705,7 +705,8 @@ impl LanguageServer {
.map(|kind| match kind { .map(|kind| match kind {
SuggestionKind::Type(t) => t.to_string(), SuggestionKind::Type(t) => t.to_string(),
SuggestionKind::Command(cmd) => cmd.to_string(), SuggestionKind::Command(cmd) => cmd.to_string(),
SuggestionKind::Module => "".to_string(), SuggestionKind::Module => "module".to_string(),
SuggestionKind::Operator => "operator".to_string(),
}) })
.map(|s| CompletionItemLabelDetails { .map(|s| CompletionItemLabelDetails {
detail: None, detail: None,
@ -754,6 +755,7 @@ impl LanguageServer {
nu_protocol::engine::CommandType::External => Some(CompletionItemKind::INTERFACE), nu_protocol::engine::CommandType::External => Some(CompletionItemKind::INTERFACE),
_ => None, _ => None,
}, },
SuggestionKind::Operator => Some(CompletionItemKind::OPERATOR),
SuggestionKind::Module => Some(CompletionItemKind::MODULE), SuggestionKind::Module => Some(CompletionItemKind::MODULE),
}) })
} }

View File

@ -35,6 +35,8 @@ num-format = { workspace = true }
rmp-serde = { workspace = true, optional = true } rmp-serde = { workspace = true, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
thiserror = "2.0" thiserror = "2.0"
typetag = "0.2" typetag = "0.2"
os_pipe = { workspace = true, optional = true, features = ["io_safety"] } os_pipe = { workspace = true, optional = true, features = ["io_safety"] }
@ -64,8 +66,6 @@ plugin = [
[dev-dependencies] [dev-dependencies]
serde_json = { workspace = true } serde_json = { workspace = true }
strum = "0.26"
strum_macros = "0.26"
nu-test-support = { path = "../nu-test-support", version = "0.102.1" } nu-test-support = { path = "../nu-test-support", version = "0.102.1" }
nu-utils = { path = "../nu-utils", version = "0.102.1" } nu-utils = { path = "../nu-utils", version = "0.102.1" }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }

View File

@ -2,22 +2,37 @@ use super::{Expr, Expression};
use crate::{ShellError, Span}; use crate::{ShellError, Span};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use strum_macros::{EnumIter, EnumMessage};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, EnumMessage)]
pub enum Comparison { pub enum Comparison {
#[strum(message = "Equal to")]
Equal, Equal,
#[strum(message = "Not equal to")]
NotEqual, NotEqual,
#[strum(message = "Less than")]
LessThan, LessThan,
#[strum(message = "Greater than")]
GreaterThan, GreaterThan,
#[strum(message = "Less than or equal to")]
LessThanOrEqual, LessThanOrEqual,
#[strum(message = "Greater than or equal to")]
GreaterThanOrEqual, GreaterThanOrEqual,
#[strum(message = "Contains regex match")]
RegexMatch, RegexMatch,
#[strum(message = "Does not contain regex match")]
NotRegexMatch, NotRegexMatch,
#[strum(message = "Is a member of (doesn't use regex)")]
In, In,
#[strum(message = "Is not a member of (doesn't use regex)")]
NotIn, NotIn,
#[strum(message = "Contains a value of (doesn't use regex)")]
Has, Has,
#[strum(message = "Does not contain a value of (doesn't use regex)")]
NotHas, NotHas,
#[strum(message = "Starts with")]
StartsWith, StartsWith,
#[strum(message = "Ends with")]
EndsWith, EndsWith,
} }
@ -42,21 +57,35 @@ impl Comparison {
} }
} }
impl AsRef<str> for Comparison {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Comparison { impl fmt::Display for Comparison {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str()) f.write_str(self.as_str())
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, EnumMessage)]
pub enum Math { pub enum Math {
#[strum(message = "Add (Plus)")]
Add, Add,
#[strum(message = "Subtract (Minus)")]
Subtract, Subtract,
#[strum(message = "Multiply")]
Multiply, Multiply,
#[strum(message = "Divide")]
Divide, Divide,
#[strum(message = "Floor division")]
FloorDivide, FloorDivide,
#[strum(message = "Floor division remainder (Modulo)")]
Modulo, Modulo,
#[strum(message = "Power of")]
Pow, Pow,
#[strum(message = "Concatenates two lists, two strings, or two binary values")]
Concatenate, Concatenate,
} }
@ -75,16 +104,25 @@ impl Math {
} }
} }
impl AsRef<str> for Math {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Math { impl fmt::Display for Math {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str()) f.write_str(self.as_str())
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, EnumMessage)]
pub enum Boolean { pub enum Boolean {
#[strum(message = "Logical OR (short-circuiting)")]
Or, Or,
#[strum(message = "Logical XOR")]
Xor, Xor,
#[strum(message = "Logical AND (short-circuiting)")]
And, And,
} }
@ -98,21 +136,38 @@ impl Boolean {
} }
} }
impl AsRef<str> for Boolean {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Boolean { impl fmt::Display for Boolean {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str()) f.write_str(self.as_str())
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, EnumMessage)]
pub enum Bits { pub enum Bits {
#[strum(message = "Bitwise OR")]
BitOr, BitOr,
#[strum(message = "Bitwise exclusive OR")]
BitXor, BitXor,
#[strum(message = "Bitwise AND")]
BitAnd, BitAnd,
#[strum(message = "Bitwise shift left")]
ShiftLeft, ShiftLeft,
#[strum(message = "Bitwise shift right")]
ShiftRight, ShiftRight,
} }
impl AsRef<str> for Bits {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Bits { impl Bits {
pub const fn as_str(&self) -> &'static str { pub const fn as_str(&self) -> &'static str {
match self { match self {
@ -131,16 +186,28 @@ impl fmt::Display for Bits {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumIter, EnumMessage)]
pub enum Assignment { pub enum Assignment {
#[strum(message = "Assigns a value to a variable.")]
Assign, Assign,
#[strum(message = "Adds a value to a variable.")]
AddAssign, AddAssign,
#[strum(message = "Subtracts a value from a variable.")]
SubtractAssign, SubtractAssign,
#[strum(message = "Multiplies a variable by a value")]
MultiplyAssign, MultiplyAssign,
#[strum(message = "Divides a variable by a value.")]
DivideAssign, DivideAssign,
#[strum(message = "Concatenates a variable with a list, string or binary.")]
ConcatenateAssign, ConcatenateAssign,
} }
impl AsRef<str> for Assignment {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Assignment { impl Assignment {
pub const fn as_str(&self) -> &'static str { pub const fn as_str(&self) -> &'static str {
match self { match self {
@ -221,7 +288,7 @@ impl fmt::Display for Operator {
} }
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, EnumIter)]
pub enum RangeInclusion { pub enum RangeInclusion {
Inclusive, Inclusive,
RightExclusive, RightExclusive,