From 945e9511ce72a4dfef9b5a8b2d498ba63f8bfc3a Mon Sep 17 00:00:00 2001 From: Tyler Miller Date: Wed, 29 Jan 2025 22:15:38 -0800 Subject: [PATCH] fix(cli): completion in nested blocks (#14856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Fixes completion for when the cursor is inside a block: ```nu foo | each { open - } ``` ```nu print (open -) print [5, 'foo', (open -)] ``` etc. Fixes: #11084 Related: #13897 (partially fixes—leading `|` is a different issue) Related: #14643 (different issue not fixed by this pr) Related: #14822 ## User-Facing Changes Flag/command completion (internal) inside blocks has been fixed. ## Tests + Formatting As far as I can tell there is only 1 test that's failing (locally), but it has nothing to do with my pr and is failing before my changes are applied. The test is `completions::variables_completions`. It's because I'm missing `$nu.user-autoload-dirs`. --- crates/nu-cli/src/completions/completer.rs | 39 ++++++++++++++++++---- crates/nu-cli/tests/completions/mod.rs | 28 ++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index f0cb6f3fda..71125e04e9 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -2,6 +2,7 @@ use crate::completions::{ 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}; use nu_engine::eval_block; use nu_parser::{flatten_pipeline_element, parse, FlatShape}; @@ -52,6 +53,11 @@ impl NuCompleter { ..Default::default() }; + debug!( + "process_completion: prefix: {}, new_span: {new_span:?}, offset: {offset}, pos: {pos}", + String::from_utf8_lossy(prefix) + ); + completer.fetch( working_set, &self.stack, @@ -134,9 +140,29 @@ impl NuCompleter { let config = self.engine_state.get_config(); - let output = parse(&mut working_set, Some("completer"), line.as_bytes(), false); + let outermost_block = parse(&mut working_set, Some("completer"), line.as_bytes(), false); - for pipeline in &output.pipelines { + // Try to get the innermost block parsed (by span) so that we consider the correct context/scope. + let target_block = working_set + .delta + .blocks + .iter() + .filter_map(|block| match block.span { + Some(span) if span.contains(pos) => Some((block, span)), + _ => None, + }) + .reduce(|prev, cur| { + // |(block, span), (block, span)| + match cur.1.start.cmp(&prev.1.start) { + core::cmp::Ordering::Greater => cur, + core::cmp::Ordering::Equal if cur.1.end < prev.1.end => cur, + _ => prev, + } + }) + .map(|(block, _)| block) + .unwrap_or(&outermost_block); + + for pipeline in &target_block.pipelines { for pipeline_element in &pipeline.elements { let flattened = flatten_pipeline_element(&working_set, pipeline_element); let mut spans: Vec = vec![]; @@ -146,10 +172,10 @@ impl NuCompleter { .first() .filter(|content| content.as_str() == "sudo" || content.as_str() == "doas") .is_some(); - // 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); + // Read the current span to string + let current_span = working_set.get_span_contents(flat.0); + let current_span_str = String::from_utf8_lossy(current_span); let is_last_span = pos >= flat.0.start && pos < flat.0.end; // Skip the last 'a' as span item @@ -176,9 +202,8 @@ impl NuCompleter { let new_span = Span::new(flat.0.start, flat.0.end - 1); // Parses the prefix. Completion should look up to the cursor position, not after. - let mut prefix = working_set.get_span_contents(flat.0); let index = pos - flat.0.start; - prefix = &prefix[..index]; + let prefix = ¤t_span[..index]; // Variables completion if prefix.starts_with(b"$") || most_left_var.is_some() { diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index b9d9fdb573..fa868a1d11 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -1862,3 +1862,31 @@ fn alias_offset_bug_7754() { // This crashes before PR #7756 let _suggestions = completer.complete("ll -a | c", 9); } + +#[rstest] +fn nested_block(mut completer: NuCompleter) { + let expected: Vec = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()]; + + let suggestions = completer.complete("somecmd | lines | each { tst - }", 30); + match_suggestions(&expected, &suggestions); + + let suggestions = completer.complete("somecmd | lines | each { tst -}", 30); + match_suggestions(&expected, &suggestions); +} + +#[rstest] +fn incomplete_nested_block(mut completer: NuCompleter) { + let suggestions = completer.complete("somecmd | lines | each { tst -", 30); + let expected: Vec = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()]; + match_suggestions(&expected, &suggestions); +} + +#[rstest] +fn deeply_nested_block(mut completer: NuCompleter) { + let suggestions = completer.complete( + "somecmd | lines | each { print ([each (print) (tst -)]) }", + 52, + ); + let expected: Vec = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()]; + match_suggestions(&expected, &suggestions); +}