diff --git a/crates/nu-cli/src/completions/cell_path_completions.rs b/crates/nu-cli/src/completions/cell_path_completions.rs new file mode 100644 index 0000000000..3d921e8bbd --- /dev/null +++ b/crates/nu-cli/src/completions/cell_path_completions.rs @@ -0,0 +1,97 @@ +use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; +use nu_engine::{column::get_columns, eval_variable}; +use nu_protocol::{ + ast::{Expr, FullCellPath, PathMember}, + engine::{Stack, StateWorkingSet}, + eval_const::eval_constant, + Span, Value, +}; +use reedline::Suggestion; + +use super::completion_options::NuMatcher; + +pub struct CellPathCompletion<'a> { + pub full_cell_path: &'a FullCellPath, +} + +impl Completer for CellPathCompletion<'_> { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + stack: &Stack, + _prefix: &[u8], + _span: Span, + offset: usize, + _pos: usize, + options: &CompletionOptions, + ) -> Vec { + // empty tail is already handled as variable names completion + let Some((prefix_member, path_members)) = self.full_cell_path.tail.split_last() else { + return vec![]; + }; + let (mut prefix_str, span) = match prefix_member { + PathMember::String { val, span, .. } => (val.clone(), span), + PathMember::Int { val, span, .. } => (val.to_string(), span), + }; + // strip the placeholder + prefix_str.pop(); + let true_end = std::cmp::max(span.start, span.end - 1); + let span = Span::new(span.start, true_end); + let current_span = reedline::Span { + start: span.start - offset, + end: true_end - offset, + }; + + let mut matcher = NuMatcher::new(prefix_str, options.clone()); + + // evaluate the head expression to get its value + let value = if let Expr::Var(var_id) = self.full_cell_path.head.expr { + working_set + .get_variable(var_id) + .const_val + .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(); + + for suggestion in nested_suggestions(&value, path_members, current_span) { + matcher.add_semantic_suggestion(suggestion); + } + matcher.results() + } +} + +// Find recursively the values for cell_path +fn nested_suggestions( + val: &Value, + path_members: &[PathMember], + current_span: reedline::Span, +) -> Vec { + let value = val + .clone() + .follow_cell_path(path_members, false) + .unwrap_or_default(); + + let kind = SuggestionKind::Type(value.get_type()); + let str_to_suggestion = |s: String| SemanticSuggestion { + suggestion: Suggestion { + value: s, + span: current_span, + ..Suggestion::default() + }, + kind: Some(kind.to_owned()), + }; + match value { + Value::Record { val, .. } => val + .columns() + .map(|s| str_to_suggestion(s.to_string())) + .collect(), + Value::List { vals, .. } => get_columns(vals.as_slice()) + .into_iter() + .map(str_to_suggestion) + .collect(), + _ => vec![], + } +} diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 9ab0cba566..9fe5d05001 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -1,6 +1,7 @@ use crate::completions::{ - CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion, - DotNuCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion, + CellPathCompletion, CommandCompletion, Completer, CompletionOptions, CustomCompletion, + DirectoryCompletion, DotNuCompletion, FileCompletion, FlagCompletion, OperatorCompletion, + VariableCompletion, }; use log::debug; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; @@ -17,6 +18,10 @@ use std::{str, sync::Arc}; use super::base::{SemanticSuggestion, SuggestionKind}; +/// Used as the function `f` in find_map Traverse +/// +/// returns the inner-most pipeline_element of interest +/// i.e. the one that contains given position and needs completion fn find_pipeline_element_by_position<'a>( expr: &'a Expression, working_set: &'a StateWorkingSet, @@ -62,6 +67,15 @@ fn find_pipeline_element_by_position<'a>( } } +/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results. +/// This function helps to strip it +fn strip_placeholder<'a>(working_set: &'a StateWorkingSet, span: &Span) -> (Span, &'a [u8]) { + let new_end = std::cmp::max(span.end - 1, span.start); + let new_span = Span::new(span.start, new_end); + let prefix = working_set.get_span_contents(new_span); + (new_span, prefix) +} + #[derive(Clone)] pub struct NuCompleter { engine_state: Arc, @@ -80,6 +94,28 @@ impl NuCompleter { self.completion_helper(line, pos) } + fn variable_names_completion_helper( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + ) -> Vec { + let (new_span, prefix) = strip_placeholder(working_set, &span); + if !prefix.starts_with(b"$") { + return vec![]; + } + let mut variable_names_completer = VariableCompletion {}; + self.process_completion( + &mut variable_names_completer, + working_set, + prefix, + new_span, + offset, + // pos is not required + 0, + ) + } + // Process the completion for a given completer fn process_completion( &self, @@ -193,6 +229,37 @@ impl NuCompleter { return vec![]; }; + match &element_expression.expr { + Expr::Var(_) => { + return self.variable_names_completion_helper( + &working_set, + element_expression.span, + fake_offset, + ); + } + Expr::FullCellPath(full_cell_path) => { + // e.g. `$e` parsed as FullCellPath + if full_cell_path.tail.is_empty() { + return self.variable_names_completion_helper( + &working_set, + element_expression.span, + fake_offset, + ); + } else { + let mut cell_path_completer = CellPathCompletion { full_cell_path }; + return self.process_completion( + &mut cell_path_completer, + &working_set, + &[], + element_expression.span, + fake_offset, + pos, + ); + } + } + _ => (), + } + let flattened = flatten_expression(&working_set, element_expression); let mut spans: Vec = vec![]; @@ -223,9 +290,6 @@ impl NuCompleter { // Complete based on the last span if is_last_span { - // Context variables - let most_left_var = most_left_variable(flat_idx, &working_set, flattened.clone()); - // Create a new span let new_span = Span::new(span.start, span.end - 1); @@ -233,36 +297,6 @@ impl NuCompleter { let index = pos - span.start; let prefix = ¤t_span[..index]; - // Variables completion - if prefix.starts_with(b"$") || most_left_var.is_some() { - let mut variable_names_completer = - VariableCompletion::new(most_left_var.unwrap_or((vec![], vec![]))); - - let mut variable_completions = self.process_completion( - &mut variable_names_completer, - &working_set, - prefix, - new_span, - fake_offset, - pos, - ); - - let mut variable_operations_completer = - OperatorCompletion::new(element_expression.clone()); - - let mut variable_operations_completions = self.process_completion( - &mut variable_operations_completer, - &working_set, - prefix, - new_span, - fake_offset, - pos, - ); - - variable_completions.append(&mut variable_operations_completions); - return variable_completions; - } - // Flags completion if prefix.starts_with(b"-") { // Try to complete flag internally @@ -474,56 +508,6 @@ impl ReedlineCompleter for NuCompleter { } } -// reads the most left variable returning it's name (e.g: $myvar) -// and the depth (a.b.c) -fn most_left_variable( - idx: usize, - working_set: &StateWorkingSet<'_>, - flattened: Vec<(Span, FlatShape)>, -) -> Option<(Vec, Vec>)> { - // Reverse items to read the list backwards and truncate - // because the only items that matters are the ones before the current index - let mut rev = flattened; - rev.truncate(idx); - rev = rev.into_iter().rev().collect(); - - // Store the variables and sub levels found and reverse to correct order - let mut variables_found: Vec> = vec![]; - let mut found_var = false; - for item in rev.clone() { - let result = working_set.get_span_contents(item.0).to_vec(); - - match item.1 { - FlatShape::Variable(_) => { - variables_found.push(result); - found_var = true; - - break; - } - FlatShape::String => { - variables_found.push(result); - } - _ => { - break; - } - } - } - - // If most left var was not found - if !found_var { - return None; - } - - // Reverse the order back - variables_found = variables_found.into_iter().rev().collect(); - - // Extract the variable and the sublevels - let var = variables_found.first().unwrap_or(&vec![]).to_vec(); - let sublevels: Vec> = variables_found.into_iter().skip(1).collect(); - - Some((var, sublevels)) -} - pub fn map_value_completions<'a>( list: impl Iterator, span: Span, diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 7c9f9ff483..6122657b28 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -1,4 +1,5 @@ mod base; +mod cell_path_completions; mod command_completions; mod completer; mod completion_common; @@ -12,6 +13,7 @@ mod operator_completions; mod variable_completions; pub use base::{Completer, SemanticSuggestion, SuggestionKind}; +pub use cell_path_completions::CellPathCompletion; pub use command_completions::CommandCompletion; pub use completer::NuCompleter; pub use completion_options::{CompletionOptions, MatchAlgorithm}; diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index 69e0985e80..b251f451e4 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -1,124 +1,34 @@ use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind}; -use nu_engine::{column::get_columns, eval_variable}; use nu_protocol::{ engine::{Stack, StateWorkingSet}, - Span, Value, + Span, VarId, }; use reedline::Suggestion; -use std::str; use super::completion_options::NuMatcher; -#[derive(Clone)] -pub struct VariableCompletion { - var_context: (Vec, Vec>), // tuple with $var and the sublevels (.b.c.d) -} - -impl VariableCompletion { - pub fn new(var_context: (Vec, Vec>)) -> Self { - Self { var_context } - } -} +pub struct VariableCompletion {} impl Completer for VariableCompletion { fn fetch( &mut self, working_set: &StateWorkingSet, - stack: &Stack, + _stack: &Stack, prefix: &[u8], span: Span, offset: usize, _pos: usize, options: &CompletionOptions, ) -> Vec { - let builtins = ["$nu", "$in", "$env"]; - let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or(""); - let var_id = working_set.find_variable(&self.var_context.0); + let prefix_str = String::from_utf8_lossy(prefix); + let mut matcher = NuMatcher::new(prefix_str, options.clone()); let current_span = reedline::Span { start: span.start - offset, end: span.end - offset, }; - let sublevels_count = self.var_context.1.len(); - let prefix_str = String::from_utf8_lossy(prefix); - let mut matcher = NuMatcher::new(prefix_str, options.clone()); - - // Completions for the given variable - if !var_str.is_empty() { - // Completion for $env. - if var_str == "$env" { - let env_vars = stack.get_env_vars(working_set.permanent_state); - - // Return nested values - if sublevels_count > 0 { - // Extract the target var ($env.) - let target_var = self.var_context.1[0].clone(); - let target_var_str = - str::from_utf8(&target_var).unwrap_or_default().to_string(); - - // Everything after the target var is the nested level ($env.....) - let nested_levels: Vec> = - self.var_context.1.clone().into_iter().skip(1).collect(); - - if let Some(val) = env_vars.get(&target_var_str) { - for suggestion in nested_suggestions(val, &nested_levels, current_span) { - matcher.add_semantic_suggestion(suggestion); - } - - return matcher.results(); - } - } else { - // No nesting provided, return all env vars - for env_var in env_vars { - matcher.add_semantic_suggestion(SemanticSuggestion { - suggestion: Suggestion { - value: env_var.0, - span: current_span, - ..Suggestion::default() - }, - kind: Some(SuggestionKind::Type(env_var.1.get_type())), - }); - } - - return matcher.results(); - } - } - - // Completions for $nu. - if var_str == "$nu" { - // Eval nu var - if let Ok(nuval) = eval_variable( - working_set.permanent_state, - stack, - nu_protocol::NU_VARIABLE_ID, - nu_protocol::Span::new(current_span.start, current_span.end), - ) { - for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span) - { - matcher.add_semantic_suggestion(suggestion); - } - - return matcher.results(); - } - } - - // Completion other variable types - if let Some(var_id) = var_id { - // Extract the variable value from the stack - let var = stack.get_var(var_id, Span::new(span.start, span.end)); - - // If the value exists and it's of type Record - if let Ok(value) = var { - for suggestion in nested_suggestions(&value, &self.var_context.1, current_span) - { - matcher.add_semantic_suggestion(suggestion); - } - - return matcher.results(); - } - } - } // Variable completion (e.g: $en to complete $env) + let builtins = ["$nu", "$in", "$env"]; for builtin in builtins { matcher.add_semantic_suggestion(SemanticSuggestion { suggestion: Suggestion { @@ -131,27 +41,30 @@ impl Completer for VariableCompletion { }); } + let mut add_candidate = |name, var_id: &VarId| { + matcher.add_semantic_suggestion(SemanticSuggestion { + suggestion: Suggestion { + value: String::from_utf8_lossy(name).to_string(), + span: current_span, + ..Suggestion::default() + }, + kind: Some(SuggestionKind::Type( + working_set.get_variable(*var_id).ty.clone(), + )), + }) + }; + // TODO: The following can be refactored (see find_commands_by_predicate() used in // command_completions). let mut removed_overlays = vec![]; // Working set scope vars for scope_frame in working_set.delta.scope.iter().rev() { for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { - for v in &overlay_frame.vars { - matcher.add_semantic_suggestion(SemanticSuggestion { - suggestion: Suggestion { - value: String::from_utf8_lossy(v.0).to_string(), - span: current_span, - ..Suggestion::default() - }, - kind: Some(SuggestionKind::Type( - working_set.get_variable(*v.1).ty.clone(), - )), - }); + for (name, var_id) in &overlay_frame.vars { + add_candidate(name, var_id); } } } - // Permanent state vars // for scope in &self.engine_state.scope { for overlay_frame in working_set @@ -159,98 +72,11 @@ impl Completer for VariableCompletion { .active_overlays(&removed_overlays) .rev() { - for v in &overlay_frame.vars { - matcher.add_semantic_suggestion(SemanticSuggestion { - suggestion: Suggestion { - value: String::from_utf8_lossy(v.0).to_string(), - span: current_span, - ..Suggestion::default() - }, - kind: Some(SuggestionKind::Type( - working_set.get_variable(*v.1).ty.clone(), - )), - }); + for (name, var_id) in &overlay_frame.vars { + add_candidate(name, var_id); } } matcher.results() } } - -// Find recursively the values for sublevels -// if no sublevels are set it returns the current value -fn nested_suggestions( - val: &Value, - sublevels: &[Vec], - current_span: reedline::Span, -) -> Vec { - let mut output: Vec = vec![]; - let value = recursive_value(val, sublevels).unwrap_or_else(Value::nothing); - - let kind = SuggestionKind::Type(value.get_type()); - match value { - Value::Record { val, .. } => { - // Add all the columns as completion - for col in val.columns() { - output.push(SemanticSuggestion { - suggestion: Suggestion { - value: col.clone(), - span: current_span, - ..Suggestion::default() - }, - kind: Some(kind.clone()), - }); - } - - output - } - Value::List { vals, .. } => { - for column_name in get_columns(vals.as_slice()) { - output.push(SemanticSuggestion { - suggestion: Suggestion { - value: column_name, - span: current_span, - ..Suggestion::default() - }, - kind: Some(kind.clone()), - }); - } - - output - } - _ => output, - } -} - -// Extracts the recursive value (e.g: $var.a.b.c) -fn recursive_value(val: &Value, sublevels: &[Vec]) -> Result { - // Go to next sublevel - if let Some((sublevel, next_sublevels)) = sublevels.split_first() { - let span = val.span(); - match val { - Value::Record { val, .. } => { - if let Some((_, value)) = val.iter().find(|(key, _)| key.as_bytes() == sublevel) { - // If matches try to fetch recursively the next - recursive_value(value, next_sublevels) - } else { - // Current sublevel value not found - Err(span) - } - } - Value::List { vals, .. } => { - for col in get_columns(vals.as_slice()) { - if col.as_bytes() == *sublevel { - let val = val.get_data_by_key(&col).ok_or(span)?; - return recursive_value(&val, next_sublevels); - } - } - - // Current sublevel value not found - Err(span) - } - _ => Ok(val.clone()), - } - } else { - Ok(val.clone()) - } -} diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index ab9152172e..fdea8b521e 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -1554,6 +1554,58 @@ fn variables_completions() { match_suggestions(&expected, &suggestions); } +#[test] +fn record_cell_path_completions() { + let (_, _, mut engine, mut stack) = new_engine(); + let command = r#"let foo = {a: [1 {a: 2}]}; const bar = {a: [1 {a: 2}]}"#; + assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok()); + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let expected: Vec = vec!["a".into()]; + let completion_str = "$foo."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let completion_str = "$foo.a.1."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let completion_str = "$bar."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let completion_str = "$bar.a.1."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let completion_str = "{a: [1 {a: 2}]}.a.1."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); +} + +#[test] +fn table_cell_path_completions() { + let (_, _, mut engine, mut stack) = new_engine(); + let command = r#"let foo = [{a:{b:1}}, {a:{b:2}}]; const bar = [[a b]; [1 2]]"#; + assert!(support::merge_input(command.as_bytes(), &mut engine, &mut stack).is_ok()); + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let expected: Vec = vec!["a".into()]; + let completion_str = "$foo."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let expected: Vec = vec!["b".into()]; + let completion_str = "$foo.a."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); + + let expected: Vec = vec!["a".into(), "b".into()]; + let completion_str = "$bar."; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&expected, &suggestions); +} + #[test] fn alias_of_command_and_flags() { let (_, _, mut engine, mut stack) = new_engine();