diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index aa342b720..e78d0ae04 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -2,10 +2,11 @@ use crate::completions::{ CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion, }; +use nu_engine::eval_block; use nu_parser::{flatten_expression, parse, FlatShape}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - Span, + BlockId, PipelineData, Span, Value, }; use reedline::{Completer as ReedlineCompleter, Suggestion}; use std::str; @@ -56,6 +57,67 @@ impl NuCompleter { suggestions } + fn external_completion( + &self, + block_id: BlockId, + spans: Vec, + offset: usize, + span: Span, + ) -> Vec { + let stack = self.stack.clone(); + let block = self.engine_state.get_block(block_id); + let mut callee_stack = stack.gather_captures(&block.captures); + + // Line + if let Some(pos_arg) = block.signature.required_positional.get(0) { + if let Some(var_id) = pos_arg.var_id { + callee_stack.add_var( + var_id, + Value::List { + vals: spans + .into_iter() + .map(|it| Value::String { + val: it, + span: Span::unknown(), + }) + .collect(), + span: Span::unknown(), + }, + ); + } + } + + let result = eval_block( + &self.engine_state, + &mut callee_stack, + block, + PipelineData::new(span), + true, + true, + ); + + match result { + Ok(pd) => { + let value = pd.into_value(span); + if let Value::List { vals, span: _ } = value { + let result = map_value_completions( + vals.iter(), + Span { + start: span.start, + end: span.end, + }, + offset, + ); + + return result; + } + } + Err(err) => println!("failed to eval completer block: {}", err), + } + + vec![] + } + fn completion_helper(&mut self, line: &str, pos: usize) -> Vec { let mut working_set = StateWorkingSet::new(&self.engine_state); let offset = working_set.next_span_start(); @@ -63,14 +125,32 @@ impl NuCompleter { let initial_line = line.to_string(); new_line.push(b'a'); let pos = offset + pos; + let config = self.engine_state.get_config(); + let (output, _err) = parse(&mut working_set, Some("completer"), &new_line, false, &[]); for pipeline in output.pipelines.into_iter() { for expr in pipeline.expressions { let flattened: Vec<_> = flatten_expression(&working_set, &expr); let span_offset: usize = alias_offset.iter().sum(); + let mut spans: Vec = vec![]; for (flat_idx, flat) in flattened.iter().enumerate() { + // Read the current spam to string + let current_span = working_set.get_span_contents(flat.0).to_vec(); + let current_span_str = String::from_utf8_lossy(¤t_span); + + // Skip the last 'a' as span item + if flat_idx == flattened.len() - 1 { + let mut chars = current_span_str.chars(); + chars.next_back(); + let current_span_str = chars.as_str().to_owned(); + spans.push(current_span_str.to_string()); + } else { + spans.push(current_span_str.to_string()); + } + + // Complete based on the last span if pos + span_offset >= flat.0.start && pos + span_offset < flat.0.end { // Context variables let most_left_var = @@ -113,16 +193,26 @@ impl NuCompleter { // Flags completion if prefix.starts_with(b"-") { - let mut completer = FlagCompletion::new(expr); - - return self.process_completion( + // Try to complete flag internally + let mut completer = FlagCompletion::new(expr.clone()); + let result = self.process_completion( &mut completer, &working_set, - prefix, + prefix.clone(), new_span, offset, pos, ); + + if !result.is_empty() { + return result; + } + + // We got no results for internal completion + // now we can check if external completer is set and use it + if let Some(block_id) = config.external_completer { + return self.external_completion(block_id, spans, offset, new_span); + } } // Completions that depends on the previous expression (e.g: use, source) @@ -214,7 +304,7 @@ impl NuCompleter { flat_shape.clone(), ); - let out: Vec<_> = self.process_completion( + let mut out: Vec<_> = self.process_completion( &mut completer, &working_set, prefix.clone(), @@ -223,21 +313,30 @@ impl NuCompleter { pos, ); - if out.is_empty() { - let mut completer = - FileCompletion::new(self.engine_state.clone()); - - return self.process_completion( - &mut completer, - &working_set, - prefix, - new_span, - offset, - pos, - ); + if !out.is_empty() { + return out; } - return out; + // Check for file completion + let mut completer = FileCompletion::new(self.engine_state.clone()); + out = self.process_completion( + &mut completer, + &working_set, + prefix, + new_span, + offset, + pos, + ); + + if !out.is_empty() { + return out; + } + + // Try to complete using an exnternal compelter (if set) + if let Some(block_id) = config.external_completer { + return self + .external_completion(block_id, spans, offset, new_span); + } } }; } @@ -383,3 +482,65 @@ fn most_left_variable( Some((var, sublevels)) } + +pub fn map_value_completions<'a>( + list: impl Iterator, + span: Span, + offset: usize, +) -> Vec { + list.filter_map(move |x| { + // Match for string values + if let Ok(s) = x.as_string() { + return Some(Suggestion { + value: s, + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + }); + } + + // Match for record values + if let Ok((cols, vals)) = x.as_record() { + let mut suggestion = Suggestion { + value: String::from(""), // Initialize with empty string + description: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + }; + + // Iterate the cols looking for `value` and `description` + cols.iter().zip(vals).for_each(|it| { + // Match `value` column + if it.0 == "value" { + // Convert the value to string + if let Ok(val_str) = it.1.as_string() { + // Update the suggestion value + suggestion.value = val_str; + } + } + + // Match `description` column + if it.0 == "description" { + // Convert the value to string + if let Ok(desc_str) = it.1.as_string() { + // Update the suggestion value + suggestion.description = Some(desc_str); + } + } + }); + + return Some(suggestion); + } + + None + }) + .collect() +} diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index 349fa4fe0..2dd465960 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -8,6 +8,8 @@ use nu_protocol::{ use reedline::Suggestion; use std::sync::Arc; +use super::completer::map_value_completions; + pub struct CustomCompletion { engine_state: Arc, stack: Stack, @@ -26,69 +28,6 @@ impl CustomCompletion { sort_by: SortBy::None, } } - - fn map_completions<'a>( - &self, - list: impl Iterator, - span: Span, - offset: usize, - ) -> Vec { - list.filter_map(move |x| { - // Match for string values - if let Ok(s) = x.as_string() { - return Some(Suggestion { - value: s, - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - append_whitespace: false, - }); - } - - // Match for record values - if let Ok((cols, vals)) = x.as_record() { - let mut suggestion = Suggestion { - value: String::from(""), // Initialize with empty string - description: None, - extra: None, - span: reedline::Span { - start: span.start - offset, - end: span.end - offset, - }, - append_whitespace: false, - }; - - // Iterate the cols looking for `value` and `description` - cols.iter().zip(vals).for_each(|it| { - // Match `value` column - if it.0 == "value" { - // Convert the value to string - if let Ok(val_str) = it.1.as_string() { - // Update the suggestion value - suggestion.value = val_str; - } - } - - // Match `description` column - if it.0 == "description" { - // Convert the value to string - if let Ok(desc_str) = it.1.as_string() { - // Update the suggestion value - suggestion.description = Some(desc_str); - } - } - }); - - return Some(suggestion); - } - - None - }) - .collect() - } } impl Completer for CustomCompletion { @@ -144,7 +83,7 @@ impl Completer for CustomCompletion { .and_then(|val| { val.as_list() .ok() - .map(|it| self.map_completions(it.iter(), span, offset)) + .map(|it| map_value_completions(it.iter(), span, offset)) }) .unwrap_or_default(); let options = value.get_data_by_key("options"); @@ -189,7 +128,7 @@ impl Completer for CustomCompletion { completions } - Value::List { vals, .. } => self.map_completions(vals.iter(), span, offset), + Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset), _ => vec![], } } diff --git a/crates/nu-cli/tests/external_completions.rs b/crates/nu-cli/tests/external_completions.rs new file mode 100644 index 000000000..a741e4b3f --- /dev/null +++ b/crates/nu-cli/tests/external_completions.rs @@ -0,0 +1,73 @@ +pub mod support; + +use nu_cli::NuCompleter; +use nu_parser::parse; +use nu_protocol::engine::StateWorkingSet; +use reedline::{Completer, Suggestion}; +use support::new_engine; + +#[test] +#[ignore] +fn external_completer_trailing_space() { + // https://github.com/nushell/nushell/issues/6378 + let block = "let external_completer = {|spans| $spans}"; + let input = "gh alias ".to_string(); + + let suggestions = run_completion(&block, &input); + assert_eq!(3, suggestions.len()); + assert_eq!("gh", suggestions.get(0).unwrap().value); + assert_eq!("alias", suggestions.get(1).unwrap().value); + assert_eq!("", suggestions.get(2).unwrap().value); +} + +#[test] +fn external_completer_no_trailing_space() { + let block = "let external_completer = {|spans| $spans}"; + let input = "gh alias".to_string(); + + let suggestions = run_completion(&block, &input); + assert_eq!(2, suggestions.len()); + assert_eq!("gh", suggestions.get(0).unwrap().value); + assert_eq!("alias", suggestions.get(1).unwrap().value); +} + +#[test] +fn external_completer_pass_flags() { + let block = "let external_completer = {|spans| $spans}"; + let input = "gh api --".to_string(); + + let suggestions = run_completion(&block, &input); + assert_eq!(3, suggestions.len()); + assert_eq!("gh", suggestions.get(0).unwrap().value); + assert_eq!("api", suggestions.get(1).unwrap().value); + assert_eq!("--", suggestions.get(2).unwrap().value); +} + +fn run_completion(block: &str, input: &str) -> Vec { + // Create a new engine + let (dir, _, mut engine_state, mut stack) = new_engine(); + let (_, delta) = { + let mut working_set = StateWorkingSet::new(&engine_state); + let (block, err) = parse(&mut working_set, None, block.as_bytes(), false, &[]); + assert!(err.is_none()); + + (block, working_set.render()) + }; + + assert!(engine_state.merge_delta(delta).is_ok()); + + // Merge environment into the permanent state + assert!(engine_state.merge_env(&mut stack, &dir).is_ok()); + + let latest_block_id = engine_state.num_blocks() - 1; + + // Change config adding the external completer + let mut config = engine_state.get_config().clone(); + config.external_completer = Some(latest_block_id); + engine_state.set_config(&config); + + // Instatiate a new completer + let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack); + + completer.complete(&input, input.len()) +} diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 25900e0e6..fd4289d25 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -52,6 +52,7 @@ impl Default for Hooks { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { + pub external_completer: Option, pub filesize_metric: bool, pub table_mode: String, pub use_ls_colors: bool, @@ -90,6 +91,7 @@ impl Default for Config { Config { filesize_metric: false, table_mode: "rounded".into(), + external_completer: None, use_ls_colors: true, color_config: HashMap::new(), use_grid_icons: false, @@ -183,6 +185,11 @@ impl Value { eprintln!("$config.filesize_metric is not a bool") } } + "external_completer" => { + if let Ok(v) = value.as_block() { + config.external_completer = Some(v) + } + } "table_mode" => { if let Ok(v) = value.as_string() { config.table_mode = v; diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 8fcf90426..c2ed421b7 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -231,8 +231,15 @@ let light_theme = { shape_nothing: light_cyan } +# External completer example +# let carapace_completer = {|spans| +# carapace $spans.0 nushell $spans | from json +# } + + # The default config record. This is where much of your global configuration is setup. let-env config = { + external_completer: $nothing # check 'carapace_completer' above to as example filesize_metric: false table_mode: rounded # basic, compact, compact_double, light, thin, with_love, rounded, reinforced, heavy, none, other use_ls_colors: true