diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 16b1673a4f..f0cb6f3fda 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -99,18 +99,24 @@ impl NuCompleter { ); match result.and_then(|data| data.into_value(span)) { - Ok(value) => { - if let Value::List { vals, .. } = value { - let result = - map_value_completions(vals.iter(), Span::new(span.start, span.end), offset); - - return Some(result); - } + Ok(Value::List { vals, .. }) => { + let result = + map_value_completions(vals.iter(), Span::new(span.start, span.end), offset); + Some(result) + } + Ok(Value::Nothing { .. }) => None, + Ok(value) => { + log::error!( + "External completer returned invalid value of type {}", + value.get_type().to_string() + ); + Some(vec![]) + } + Err(err) => { + log::error!("failed to eval completer block: {err}"); + Some(vec![]) } - Err(err) => println!("failed to eval completer block: {err}"), } - - None } fn completion_helper(&mut self, line: &str, pos: usize) -> Vec { @@ -319,6 +325,7 @@ impl NuCompleter { self.stack.clone(), *decl_id, initial_line, + FileCompletion::new(), ); return self.process_completion( diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index 2a9c621192..852ea130f8 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -12,32 +12,34 @@ use std::collections::HashMap; use super::completion_options::NuMatcher; -pub struct CustomCompletion { +pub struct CustomCompletion { stack: Stack, decl_id: DeclId, line: String, + fallback: T, } -impl CustomCompletion { - pub fn new(stack: Stack, decl_id: DeclId, line: String) -> Self { +impl CustomCompletion { + pub fn new(stack: Stack, decl_id: DeclId, line: String, fallback: T) -> Self { Self { stack, decl_id, line, + fallback, } } } -impl Completer for CustomCompletion { +impl Completer for CustomCompletion { fn fetch( &mut self, working_set: &StateWorkingSet, - _stack: &Stack, + stack: &Stack, prefix: &[u8], span: Span, offset: usize, pos: usize, - completion_options: &CompletionOptions, + orig_options: &CompletionOptions, ) -> Vec { // Line position let line_pos = pos - offset; @@ -66,13 +68,12 @@ impl Completer for CustomCompletion { PipelineData::empty(), ); - let mut completion_options = completion_options.clone(); + let mut completion_options = orig_options.clone(); let mut should_sort = true; // Parse result - let suggestions = result - .and_then(|data| data.into_value(span)) - .map(|value| match &value { + let suggestions = match result.and_then(|data| data.into_value(span)) { + Ok(value) => match &value { Value::Record { val, .. } => { let completions = val .get("completions") @@ -112,9 +113,30 @@ impl Completer for CustomCompletion { completions } Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset), - _ => vec![], - }) - .unwrap_or_default(); + Value::Nothing { .. } => { + return self.fallback.fetch( + working_set, + stack, + prefix, + span, + offset, + pos, + orig_options, + ); + } + _ => { + log::error!( + "Custom completer returned invalid value of type {}", + value.get_type().to_string() + ); + return vec![]; + } + }, + Err(e) => { + log::error!("Error getting custom completions: {e}"); + return vec![]; + } + }; let mut matcher = NuMatcher::new(String::from_utf8_lossy(prefix), completion_options); diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index c8a3ea6d84..b9d9fdb573 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -234,6 +234,37 @@ fn customcompletions_no_sort() { match_suggestions(&expected, &suggestions); } +/// Fallback to file completions if custom completer returns null +#[test] +fn customcompletions_fallback() { + let (_, _, mut engine, mut stack) = new_engine(); + let command = r#" + def comp [] { null } + def my-command [arg: string@comp] {}"#; + 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 completion_str = "my-command test"; + let suggestions = completer.complete(completion_str, completion_str.len()); + let expected: Vec = vec![folder("test_a"), file("test_a_symlink"), folder("test_b")]; + match_suggestions(&expected, &suggestions); +} + +/// Suppress completions for invalid values +#[test] +fn customcompletions_invalid() { + let (_, _, mut engine, mut stack) = new_engine(); + let command = r#" + def comp [] { 123 } + def my-command [arg: string@comp] {}"#; + 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 completion_str = "my-command foo"; + let suggestions = completer.complete(completion_str, completion_str.len()); + assert!(suggestions.is_empty()); +} + #[test] fn dotnu_completions() { // Create a new engine @@ -312,6 +343,27 @@ fn external_completer_pass_flags() { assert_eq!("--", suggestions.get(2).unwrap().value); } +/// Fallback to file completions when external completer returns null +#[test] +fn external_completer_fallback() { + let block = "{|spans| null}"; + let input = "foo test".to_string(); + + let expected = vec![folder("test_a"), file("test_a_symlink"), folder("test_b")]; + let suggestions = run_external_completion(block, &input); + match_suggestions(&expected, &suggestions); +} + +/// Suppress completions when external completer returns invalid value +#[test] +fn external_completer_invalid() { + let block = "{|spans| 123}"; + let input = "foo ".to_string(); + + let suggestions = run_external_completion(block, &input); + assert!(suggestions.is_empty()); +} + #[test] fn file_completions() { // Create a new engine