fix(cli): completion in nested blocks (#14856)

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

## Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

Fixes completion for when the cursor is inside a block:

```nu
foo | each { open -<Tab> }
```

```nu
print (open -<Tab>)
print [5, 'foo', (open -<Tab>)]
```

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
<!--
Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

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`.
This commit is contained in:
Tyler Miller 2025-01-29 22:15:38 -08:00 committed by GitHub
parent cce12efe48
commit 945e9511ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 7 deletions

View File

@ -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<String> = 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(&current_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 = &current_span[..index];
// Variables completion
if prefix.starts_with(b"$") || most_left_var.is_some() {

View File

@ -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<String> = 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<String> = 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<String> = vec!["--help".into(), "--mod".into(), "-h".into(), "-s".into()];
match_suggestions(&expected, &suggestions);
}