mirror of
https://github.com/nushell/nushell.git
synced 2025-06-19 08:26:57 +02:00
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:
parent
c5a14bb8ff
commit
52a35827c7
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3538,6 +3538,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reedline",
|
"reedline",
|
||||||
"rstest",
|
"rstest",
|
||||||
|
"strum",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
@ -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);
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user