Compare commits

..

2 Commits

Author SHA1 Message Date
0120e4040d Bump to 0.77.1 development version (#8453)
# Description

Either to be used in an emergency point release or to indicate
development builds in the `version` command
2023-03-17 18:10:56 +13:00
716c144e24 Revert "Throw out error if external command in subexpression is failed to run (#8204)" (#8475)
This reverts commit dec0a2517f.

It breaks programs like `fzf`

# Description

Fixes: #8472 
Fixes:  #8313
Reopen: #7690 

# User-Facing Changes

_(List of all changes that impact the user experience here. This helps
us keep track of breaking changes.)_

# Tests + Formatting

Don't forget to add tests that cover your changes.

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 -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

> **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
> ```

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
2023-03-17 17:42:26 +13:00
1735 changed files with 94175 additions and 157292 deletions

View File

@ -26,8 +26,3 @@ rustflags = ["-C", "link-args=-stack:10000000", "-C", "target-feature=+crt-stati
# [target.aarch64-apple-darwin] # [target.aarch64-apple-darwin]
# linker = "clang" # linker = "clang"
# rustflags = ["-C", "link-arg=-fuse-ld=mold"] # rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
# We can guarantee that this target will always run on a CPU with _at least_
# these capabilities, so let's optimize for them
rustflags = ["-Ctarget-cpu=apple-m1"]

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
# Example of a `.gitattributes` file which reclassifies `.nu` files as Nushell:
*.nu linguist-language=Nushell

View File

@ -1,5 +0,0 @@
#!/usr/bin/env nu
use ../toolkit.nu fmt
fmt --check --verbose

View File

@ -1,6 +0,0 @@
#!/usr/bin/env nu
use ../toolkit.nu [fmt, clippy]
fmt --check --verbose
clippy --verbose

View File

@ -1 +0,0 @@
This directory is intended for templates to automatically create issues with the [create-an-issue](https://github.com/JasonEtco/create-an-issue) action.

View File

@ -1,16 +0,0 @@
---
name: Nightly build of release binaries failed
about: Used to submit issues related to binaries release workflow
title: 'Attention: Nightly build of release binaries failed'
labels: ['build-package', 'priority']
assignees: ''
---
**Nightly build of release binaries failed**
Hi there:
If you see me here that means there is a release failure for the nightly build
Please **click the status badge** to see more details: [![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml)

View File

@ -1,6 +1,5 @@
name: Bug Report name: Bug Report
description: Create a report to help us improve description: Create a report to help us improve
labels: ["needs-triage"]
body: body:
- type: textarea - type: textarea
id: description id: description

View File

@ -1,6 +1,6 @@
name: Feature Request name: Feature Request
description: "When you want a new feature for something that doesn't already exist" description: "When you want a new feature for something that doesn't already exist"
labels: ["needs-triage", "enhancement"] labels: "enhancement"
body: body:
- type: textarea - type: textarea
id: problem id: problem

View File

@ -11,10 +11,6 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
# We release on Tuesdays and open dependabot PRs will rebase after the
# version bump and thus consume unnecessary workers during release, thus
# let's open new ones on Wednesday
day: "wednesday"
ignore: ignore:
- dependency-name: "*" - dependency-name: "*"
update-types: ["version-update:semver-patch"] update-types: ["version-update:semver-patch"]
@ -22,4 +18,3 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
day: "wednesday"

View File

@ -1,32 +1,23 @@
<!--
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 # 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. _(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.)_
# User-Facing Changes # User-Facing Changes
<!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. -->
_(List of all changes that impact the user experience here. This helps us keep track of breaking changes.)_
# Tests + Formatting # Tests + Formatting
<!--
Don't forget to add tests that cover your changes. Don't forget to add tests that cover your changes.
Make sure you've run and fixed any issues with these commands: 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 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 clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` 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 test --workspace` to check that all tests pass
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library
> **Note** > **Note**
> from `nushell` you can also use the `toolkit` as follows > from `nushell` you can also use the `toolkit` as follows
@ -34,7 +25,7 @@ Make sure you've run and fixed any issues with these commands:
> use toolkit.nu # or use an `env_change` hook to activate it automatically > use toolkit.nu # or use an `env_change` hook to activate it automatically
> toolkit check pr > toolkit check pr
> ``` > ```
-->
# After Submitting # After Submitting
<!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.

View File

@ -1,25 +0,0 @@
name: Security audit
on:
pull_request:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
push:
branches:
- main
env:
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
CLICOLOR: 1
jobs:
security_audit:
runs-on: ubuntu-latest
# Prevent sudden announcement of a new advisory from failing ci:
continue-on-error: true
steps:
- uses: actions/checkout@v4.1.6
- uses: rustsec/audit-check@v1.4.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,12 +0,0 @@
let toolchain_spec = open rust-toolchain.toml | get toolchain.channel
let msrv_spec = open Cargo.toml | get package.rust-version
# This check is conservative in the sense that we use `rust-toolchain.toml`'s
# override to ensure that this is the upper-bound for the minimum supported
# rust version
if $toolchain_spec != $msrv_spec {
print -e "Mismatching rust compiler versions specified in `Cargo.toml` and `rust-toolchain.toml`"
print -e $"Cargo.toml: ($msrv_spec)"
print -e $"rust-toolchain.toml: ($toolchain_spec)"
exit 1
}

View File

@ -1,169 +1,196 @@
on: on:
pull_request: pull_request:
push: push: # Run CI on the main branch after every merge. This is important to fill the GitHub Actions cache in a way that pull requests can see it
branches: branches:
- main - main
name: continuous-integration name: continuous-integration
env:
NUSHELL_CARGO_PROFILE: ci
NU_LOG_LEVEL: DEBUG
# If changing these settings also change toolkit.nu
CLIPPY_OPTIONS: "-D warnings -D clippy::unwrap_used -D clippy::unchecked_duration_subtraction"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
fmt-clippy: nu-fmt-clippy:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
# Pinning to Ubuntu 20.04 because building on newer Ubuntu versions causes linux-gnu # Pinning to Ubuntu 20.04 because building on newer Ubuntu versions causes linux-gnu
# builds to link against a too-new-for-many-Linux-installs glibc version. Consider # builds to link against a too-new-for-many-Linux-installs glibc version. Consider
# revisiting this when 20.04 is closer to EOL (April 2025) # revisiting this when 20.04 is closer to EOL (April 2025)
# platform: [windows-latest, macos-latest, ubuntu-20.04]
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, style: [default, dataframe]
# instead of 14 GB) which is too little for us right now. Revisit when `dfr` commands are rust:
# removed and we're only building the `polars` plugin instead - stable
platform: [windows-latest, macos-13, ubuntu-20.04] include:
- style: default
flags: ""
- style: dataframe
flags: "--features=dataframe "
exclude:
# only test dataframes on Ubuntu (the fastest platform)
- platform: windows-latest
style: dataframe
- platform: macos-latest
style: dataframe
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
env:
NUSHELL_CARGO_TARGET: ci
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v3
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
- name: cargo fmt - name: cargo fmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
# If changing these settings also change toolkit.nu
- name: Clippy - name: Clippy
run: cargo clippy --workspace --exclude nu_plugin_* -- $CLIPPY_OPTIONS run: cargo clippy --workspace ${{ matrix.flags }}--exclude nu_plugin_* -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect
# In tests we don't have to deny unwrap nu-tests:
- name: Clippy of tests env:
run: cargo clippy --tests --workspace --exclude nu_plugin_* -- -D warnings NUSHELL_CARGO_TARGET: ci
- name: Clippy of benchmarks
run: cargo clippy --benches --workspace --exclude nu_plugin_* -- -D warnings
tests:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
platform: [windows-latest, macos-latest, ubuntu-20.04] platform: [windows-latest, macos-latest, ubuntu-20.04]
style: [default, dataframe]
rust:
- stable
include: include:
- default-flags: "" - style: default
# linux CI cannot handle clipboard feature flags: ""
- platform: ubuntu-20.04 - style: dataframe
default-flags: "--no-default-features --features=default-no-clipboard" flags: "--features=dataframe"
exclude:
# only test dataframes on Ubuntu (the fastest platform)
- platform: windows-latest
style: dataframe
- platform: macos-latest
style: dataframe
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v3
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
- name: Tests - name: Tests
run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.default-flags }} run: cargo test --workspace --profile ci --exclude nu_plugin_* ${{ matrix.flags }}
- name: Check for clean repo
shell: bash python-virtualenv:
run: | env:
if [ -n "$(git status --porcelain)" ]; then NUSHELL_CARGO_TARGET: ci
echo "there are changes";
git status --porcelain
exit 1
else
echo "no changes in working directory";
fi
std-lib-and-python-virtualenv:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
platform: [ubuntu-20.04, macos-latest, windows-latest] platform: [ubuntu-20.04, macos-latest, windows-latest]
rust:
- stable
py: py:
- py - py
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v3
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
- name: Install Nushell - name: Install Nushell
run: cargo install --path . --locked --no-default-features run: cargo install --locked --path=. --profile ci --no-default-features
- name: Standard library tests
run: nu -c 'use crates/nu-std/testing.nu; testing run-tests --path crates/nu-std'
- name: Ensure that Cargo.toml MSRV and rust-toolchain.toml use the same version
run: nu .github/workflows/check-msrv.nu
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
- run: python -m pip install tox
# Get only the latest tagged version for stability reasons
- name: Install virtualenv - name: Install virtualenv
run: pip install virtualenv run: git clone https://github.com/pypa/virtualenv.git
shell: bash shell: bash
- name: Test Nushell in virtualenv - name: Test Nushell in virtualenv
run: nu scripts/test_virtualenv.nu
shell: bash
- name: Check for clean repo
shell: bash
run: | run: |
if [ -n "$(git status --porcelain)" ]; then cd virtualenv
echo "there are changes"; # if we encounter problems with bleeding edge tests pin to the latest tag
git status --porcelain # git checkout $(git describe --tags | cut -d - -f 1)
exit 1 # We need to disable failing on coverage levels.
else nu -c "open pyproject.toml | upsert tool.coverage.report.fail_under 1 | save patchproject.toml"
echo "no changes in working directory"; mv patchproject.toml pyproject.toml
fi tox -e ${{ matrix.py }} -- -k nushell
shell: bash
# Build+test plugins on their own, without the rest of Nu. This helps with CI parallelization and
# also helps test that the plugins build without any feature unification shenanigans
plugins: plugins:
env:
NUSHELL_CARGO_TARGET: ci
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB, platform: [windows-latest, macos-latest, ubuntu-20.04]
# instead of 14 GB) which is too little for us right now. rust:
# - stable
# Failure occurring with clippy for rust 1.77.2
platform: [windows-latest, macos-13, ubuntu-20.04]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v3
- name: Setup Rust toolchain and cache - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
- name: Clippy - name: Clippy
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS run: cargo clippy --package nu_plugin_* ${{ matrix.flags }} -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect
- name: Tests - name: Tests
run: cargo test --profile ci --package nu_plugin_* run: cargo test --profile ci --package nu_plugin_*
- name: Check for clean repo
nu-coverage:
needs: nu-tests
env:
NUSHELL_CARGO_TARGET: ci
strategy:
fail-fast: true
matrix:
# disabled mac due to problems with merging coverage and similarity to linux
# disabled windows due to running out of disk space when having too many crates or tests
platform: [ubuntu-20.04] # windows-latest
rust:
- stable
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Tests
shell: bash shell: bash
run: | run: |
if [ -n "$(git status --porcelain)" ]; then source <(cargo llvm-cov show-env --export-prefix) # Set the environment variables needed to get coverage.
echo "there are changes"; cargo llvm-cov clean --workspace # Remove artifacts that may affect the coverage results.
git status --porcelain cargo build --workspace --profile ci
exit 1 cargo test --workspace --profile ci
else cargo llvm-cov report --profile ci --lcov --output-path lcov.info
echo "no changes in working directory";
fi - name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
with:
files: lcov.info

View File

@ -1,209 +0,0 @@
#
# REF:
# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude
# 2. https://github.com/JasonEtco/create-an-issue
# 3. https://docs.github.com/en/actions/learn-github-actions/variables
# 4. https://github.com/actions/github-script
#
name: Nightly Build
on:
push:
branches:
- nightly # Just for test purpose only with the nightly repo
# This schedule will run only from the default branch
schedule:
- cron: '15 0 * * *' # run at 00:15 AM UTC
defaults:
run:
shell: bash
jobs:
prepare:
name: Prepare
runs-on: ubuntu-latest
# This job is required by the release job, so we should make it run both from Nushell repo and nightly repo
# if: github.repository == 'nushell/nightly'
steps:
- name: Checkout
uses: actions/checkout@v4.1.6
if: github.repository == 'nushell/nightly'
with:
ref: main
fetch-depth: 0
# Configure PAT here: https://github.com/settings/tokens for the push operation in the following steps
token: ${{ secrets.WORKFLOW_TOKEN }}
- name: Setup Nushell
uses: hustcer/setup-nu@v3.10
if: github.repository == 'nushell/nightly'
with:
version: 0.93.0
# Synchronize the main branch of nightly repo with the main branch of Nushell official repo
- name: Prepare for Nightly Release
shell: nu {0}
if: github.repository == 'nushell/nightly'
run: |
cd $env.GITHUB_WORKSPACE
git checkout main
# We can't push if no user name and email are configured
git config user.name 'hustcer'
git config user.email 'hustcer@outlook.com'
git pull origin main
git remote add src https://github.com/nushell/nushell.git
git fetch src main
# All the changes will be overwritten by the upstream main branch
git reset --hard src/main
git push origin main -f
let sha_short = (git rev-parse --short origin/main | str trim | str substring 0..7)
let tag_name = $'nightly-($sha_short)'
if (git ls-remote --tags origin $tag_name | is-empty) {
git tag -a $tag_name -m $'Nightly build from ($sha_short)'
git push origin --tags
}
standard:
name: Std
needs: prepare
strategy:
fail-fast: false
matrix:
target:
- aarch64-apple-darwin
- x86_64-apple-darwin
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- armv7-unknown-linux-gnueabihf
- riscv64gc-unknown-linux-gnu
extra: ['bin']
include:
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
extra: 'bin'
os: windows-latest
- target: x86_64-pc-windows-msvc
extra: msi
os: windows-latest
- target: aarch64-pc-windows-msvc
extra: 'bin'
os: windows-latest
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
- target: x86_64-unknown-linux-gnu
os: ubuntu-20.04
- target: x86_64-unknown-linux-musl
os: ubuntu-20.04
- target: aarch64-unknown-linux-gnu
os: ubuntu-20.04
- target: armv7-unknown-linux-gnueabihf
os: ubuntu-20.04
- target: riscv64gc-unknown-linux-gnu
os: ubuntu-latest
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4.1.6
with:
ref: main
fetch-depth: 0
- name: Update Rust Toolchain Target
run: |
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
rustflags: ''
- name: Setup Nushell
uses: hustcer/setup-nu@v3.10
with:
version: 0.93.0
- name: Release Nu Binary
id: nu
run: nu .github/workflows/release-pkg.nu
env:
OS: ${{ matrix.os }}
REF: ${{ github.ref }}
TARGET: ${{ matrix.target }}
_EXTRA_: ${{ matrix.extra }}
- name: Create an Issue for Release Failure
if: ${{ failure() }}
uses: JasonEtco/create-an-issue@v2.9.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
update_existing: true
search_existing: open
filename: .github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
- name: Set Outputs of Short SHA
id: vars
run: |
echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
sha_short=$(git rev-parse --short HEAD)
echo "sha_short=${sha_short:0:7}" >> $GITHUB_OUTPUT
# REF: https://github.com/marketplace/actions/gh-release
# Create a release only in nushell/nightly repo
- name: Publish Archive
uses: softprops/action-gh-release@v2.0.5
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
with:
prerelease: true
files: ${{ steps.nu.outputs.archive }}
tag_name: nightly-${{ steps.vars.outputs.sha_short }}
name: Nu-nightly-${{ steps.vars.outputs.date }}-${{ steps.vars.outputs.sha_short }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cleanup:
name: Cleanup
# Should only run in nushell/nightly repo
if: github.repository == 'nushell/nightly'
runs-on: ubuntu-latest
steps:
# Sleep for 30 minutes, waiting for the release to be published
- name: Waiting for Release
run: sleep 1800
- uses: actions/checkout@v4.1.6
with:
ref: main
- name: Setup Nushell
uses: hustcer/setup-nu@v3.10
with:
version: 0.93.0
# Keep the last a few releases
- name: Delete Older Releases
shell: nu {0}
run: |
let KEEP_COUNT = 10
let deprecated = (http get https://api.github.com/repos/nushell/nightly/releases | sort-by -r created_at | select tag_name id | range $KEEP_COUNT..)
for release in $deprecated {
print $'Deleting tag ($release.tag_name)'
git push origin --delete $release.tag_name
print $'Deleting release ($release.tag_name)'
let delete_url = $'https://api.github.com/repos/nushell/nightly/releases/($release.id)'
let version = "X-GitHub-Api-Version: 2022-11-28"
let accept = "Accept: application/vnd.github+json"
let auth = "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"
# http delete $delete_url -H $version -H $auth -H $accept
curl -L -X DELETE -H $accept -H $auth -H $version $delete_url
}

View File

@ -11,31 +11,32 @@
# Updated again on 2023-02-23 because msis are still failing validation # Updated again on 2023-02-23 because msis are still failing validation
# To run this manual for windows here are the steps I take # To run this manual for windows here are the steps I take
# checkout the release you want to publish # checkout the release you want to publish
# 1. git checkout 0.86.0 # 1. git checkout 0.76.0
# unset CARGO_TARGET_DIR if set (I have to do this in the parent shell to get it to work) # unset CARGO_TARGET_DIR if set (I have to do this in the parent shell to get it to work)
# 2. $env:CARGO_TARGET_DIR = "" # 2. $env:CARGO_TARGET_DIR = ""
# 2. hide-env CARGO_TARGET_DIR # 2. hide-env CARGO_TARGET_DIR
# 3. $env.TARGET = 'x86_64-pc-windows-msvc' # 3. let-env TARGET = 'x86_64-pc-windows-msvc'
# 4. $env.GITHUB_WORKSPACE = 'D:\nushell' # 4. let-env TARGET_RUSTFLAGS = ''
# 5. $env.GITHUB_OUTPUT = 'D:\nushell\output\out.txt' # 5. let-env GITHUB_WORKSPACE = 'C:\Users\dschroeder\source\repos\forks\nushell'
# 6. $env.OS = 'windows-latest' # 6. let-env GITHUB_OUTPUT = 'C:\Users\dschroeder\source\repos\forks\nushell\output\out.txt'
# 7. let-env OS = 'windows-latest'
# make sure 7z.exe is in your path https://www.7-zip.org/download.html # make sure 7z.exe is in your path https://www.7-zip.org/download.html
# 7. $env.Path = ($env.Path | append 'c:\apps\7-zip') # 8. let-env Path = ($env.Path | append 'c:\apps\7-zip')
# make sure aria2c.exe is in your path https://github.com/aria2/aria2 # make sure aria2c.exe is in your path https://github.com/aria2/aria2
# 8. $env.Path = ($env.Path | append 'c:\path\to\aria2c') # 9. let-env Path = ($env.Path | append 'c:\path\to\aria2c')
# make sure you have the wixtools installed https://wixtoolset.org/ # make sure you have the wixtools installed https://wixtoolset.org/
# 9. $env.Path = ($env.Path | append 'C:\Users\dschroeder\AppData\Local\tauri\WixTools') # 10. let-env Path = ($env.Path | append 'C:\Users\dschroeder\AppData\Local\tauri\WixTools')
# You need to run the release-pkg twice. The first pass, with _EXTRA_ as 'bin', makes the output # You need to run the release-pkg twice. The first pass, with _EXTRA_ as 'bin', makes the output
# folder and builds everything. The second pass, that generates the msi file, with _EXTRA_ as 'msi' # folder and builds everything. The second pass, that generates the msi file, with _EXTRA_ as 'msi'
# 10. $env._EXTRA_ = 'bin' # 11. let-env _EXTRA_ = 'bin'
# 11. source .github\workflows\release-pkg.nu # 12. source .github\workflows\release-pkg.nu
# 12. cd .. # 13. cd ..
# 13. $env._EXTRA_ = 'msi' # 14. let-env _EXTRA_ = 'msi'
# 14. source .github\workflows\release-pkg.nu # 15. source .github\workflows\release-pkg.nu
# After msi is generated, you have to update winget-pkgs repo, you'll need to patch the release # After msi is generated, you have to update winget-pkgs repo, you'll need to patch the release
# by deleting the existing msi and uploading this new msi. Then you'll need to update the hash # by deleting the existing msi and uploading this new msi. Then you'll need to update the hash
# on the winget-pkgs PR. To generate the hash, run this command # on the winget-pkgs PR. To generate the hash, run this command
# 15. open target\wix\nu-0.74.0-x86_64-pc-windows-msvc.msi | hash sha256 # 16. open target\wix\nu-0.74.0-x86_64-pc-windows-msvc.msi | hash sha256
# Then, just take the output and put it in the winget-pkgs PR for the hash on the msi # Then, just take the output and put it in the winget-pkgs PR for the hash on the msi
@ -45,51 +46,47 @@ let os = $env.OS
let target = $env.TARGET let target = $env.TARGET
# Repo source dir like `/home/runner/work/nushell/nushell` # Repo source dir like `/home/runner/work/nushell/nushell`
let src = $env.GITHUB_WORKSPACE let src = $env.GITHUB_WORKSPACE
let flags = $env.TARGET_RUSTFLAGS
let dist = $'($env.GITHUB_WORKSPACE)/output' let dist = $'($env.GITHUB_WORKSPACE)/output'
let version = (open Cargo.toml | get package.version) let version = (open Cargo.toml | get package.version)
print $'Debugging info:' $'Debugging info:'
print { version: $version, bin: $bin, os: $os, target: $target, src: $src, dist: $dist }; hr-line -b print { version: $version, bin: $bin, os: $os, target: $target, src: $src, flags: $flags, dist: $dist }; hr-line -b
# $env # $env
let USE_UBUNTU = $os starts-with ubuntu let USE_UBUNTU = 'ubuntu-20.04'
print $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b
if not ('Cargo.lock' | path exists) { cargo generate-lockfile } if not ('Cargo.lock' | path exists) { cargo generate-lockfile }
print $'Start building ($bin)...'; hr-line $'Start building ($bin)...'; hr-line
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Build for Ubuntu and macOS # Build for Ubuntu and macOS
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
if $os in ['macos-latest'] or $USE_UBUNTU { if $os in [$USE_UBUNTU, 'macos-latest'] {
if $USE_UBUNTU { if $os == $USE_UBUNTU {
sudo apt update sudo apt update
sudo apt-get install libxcb-composite0-dev -y sudo apt-get install libxcb-composite0-dev -y
} }
match $target { if $target == 'aarch64-unknown-linux-gnu' {
'aarch64-unknown-linux-gnu' => { sudo apt-get install gcc-aarch64-linux-gnu -y
sudo apt-get install gcc-aarch64-linux-gnu -y let-env CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc'
$env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc' cargo-build-nu $flags
cargo-build-nu } else if $target == 'armv7-unknown-linux-gnueabihf' {
} sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y
'riscv64gc-unknown-linux-gnu' => { let-env CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc'
sudo apt-get install gcc-riscv64-linux-gnu -y cargo-build-nu $flags
$env.CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc' } else if $target == 'riscv64gc-unknown-linux-gnu' {
cargo-build-nu sudo apt-get install gcc-riscv64-linux-gnu -y
} let-env CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc'
'armv7-unknown-linux-gnueabihf' => { cargo-build-nu $flags
sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y } else {
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc' # musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?'
cargo-build-nu # Actually just for x86_64-unknown-linux-musl target
} if $os == $USE_UBUNTU { sudo apt install musl-tools -y }
_ => { cargo-build-nu $flags
# musl-tools to fix 'Failed to find tool. Is `musl-gcc` installed?'
# Actually just for x86_64-unknown-linux-musl target
if $USE_UBUNTU { sudo apt install musl-tools -y }
cargo-build-nu
}
} }
} }
@ -97,7 +94,11 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
# Build for Windows without static-link-openssl feature # Build for Windows without static-link-openssl feature
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
if $os in ['windows-latest'] { if $os in ['windows-latest'] {
cargo-build-nu if ($flags | str trim | is-empty) {
cargo build --release --all --target $target
} else {
cargo build --release --all --target $target $flags
}
} }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@ -106,50 +107,41 @@ if $os in ['windows-latest'] {
let suffix = if $os == 'windows-latest' { '.exe' } let suffix = if $os == 'windows-latest' { '.exe' }
# nu, nu_plugin_* were all included # nu, nu_plugin_* were all included
let executable = $'target/($target)/release/($bin)*($suffix)' let executable = $'target/($target)/release/($bin)*($suffix)'
print $'Current executable file: ($executable)' $'Current executable file: ($executable)'
cd $src; mkdir $dist; cd $src; mkdir $dist;
rm -rf ...(glob $'target/($target)/release/*.d') ...(glob $'target/($target)/release/nu_pretty_hex*') rm -rf $'target/($target)/release/*.d' $'target/($target)/release/nu_pretty_hex*'
print $'(char nl)All executable files:'; hr-line $'(char nl)All executable files:'; hr-line
# We have to use `print` here to make sure the command output is displayed ls -f $executable
print (ls -f ($executable | into glob)); sleep 1sec
print $'(char nl)Copying release files...'; hr-line $'(char nl)Copying release files...'; hr-line
"To use the included Nushell plugins, register the binaries with the `plugin add` command to tell Nu where to find the plugin. cp -v README.release.txt $'($dist)/README.txt'
Then you can use `plugin use` to load the plugin into your session. [LICENSE $executable] | each {|it| cp -rv $it $dist } | flatten
For example:
> plugin add ./nu_plugin_query $'(char nl)Check binary release version detail:'; hr-line
> plugin use query
For more information, refer to https://www.nushell.sh/book/plugins.html
" | save $'($dist)/README.txt' -f
[LICENSE ...(glob $executable)] | each {|it| cp -rv $it $dist } | flatten
print $'(char nl)Check binary release version detail:'; hr-line
let ver = if $os == 'windows-latest' { let ver = if $os == 'windows-latest' {
(do -i { .\output\nu.exe -c 'version' }) | str join (do -i { ./output/nu.exe -c 'version' }) | str join
} else { } else {
(do -i { ./output/nu -c 'version' }) | str join (do -i { ./output/nu -c 'version' }) | str join
} }
if ($ver | str trim | is-empty) { if ($ver | str trim | is-empty) {
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)' $'(ansi r)Incompatible nu binary...(ansi reset)'
} else { print $ver } } else { $ver }
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Create a release archive and send it to output for the following steps # Create a release archive and send it to output for the following steps
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
cd $dist; print $'(char nl)Creating release archive...'; hr-line cd $dist; $'(char nl)Creating release archive...'; hr-line
if $os in ['macos-latest'] or $USE_UBUNTU { if $os in [$USE_UBUNTU, 'macos-latest'] {
let files = (ls | get name) let files = (ls | get name)
let dest = $'($bin)-($version)-($target)' let dest = $'($bin)-($version)-($target)'
let archive = $'($dist)/($dest).tar.gz' let archive = $'($dist)/($dest).tar.gz'
mkdir $dest mkdir $dest
$files | each {|it| cp -v $it $dest } $files | each {|it| mv $it $dest } | ignore
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls $dest | print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls $dest
tar -czf $archive $dest tar -czf $archive $dest
print $'archive: ---> ($archive)'; ls $archive print $'archive: ---> ($archive)'; ls $archive
@ -160,7 +152,7 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
let releaseStem = $'($bin)-($version)-($target)' let releaseStem = $'($bin)-($version)-($target)'
print $'(char nl)Download less related stuffs...'; hr-line $'(char nl)Download less related stuffs...'; hr-line
aria2c https://github.com/jftuga/less-Windows/releases/download/less-v608/less.exe -o less.exe aria2c https://github.com/jftuga/less-Windows/releases/download/less-v608/less.exe -o less.exe
aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt aria2c https://raw.githubusercontent.com/jftuga/less-Windows/master/LICENSE -o LICENSE-for-less.txt
@ -168,44 +160,39 @@ if $os in ['macos-latest'] or $USE_UBUNTU {
if (get-env _EXTRA_) == 'msi' { if (get-env _EXTRA_) == 'msi' {
let wixRelease = $'($src)/target/wix/($releaseStem).msi' let wixRelease = $'($src)/target/wix/($releaseStem).msi'
print $'(char nl)Start creating Windows msi package with the following contents...' $'(char nl)Start creating Windows msi package...'
cd $src; hr-line cd $src; hr-line
# Wix need the binaries be stored in target/release/ # Wix need the binaries be stored in target/release/
cp -r ($'($dist)/*' | into glob) target/release/ cp -r $'($dist)/*' target/release/
ls target/release/* | print cargo install cargo-wix --version 0.3.4
cargo install cargo-wix --version 0.3.8
cargo wix --no-build --nocapture --package nu --output $wixRelease cargo wix --no-build --nocapture --package nu --output $wixRelease
# Workaround for https://github.com/softprops/action-gh-release/issues/280 print $'archive: ---> ($wixRelease)';
let archive = ($wixRelease | str replace --all '\' '/') echo $"archive=($wixRelease)" | save --append $env.GITHUB_OUTPUT
print $'archive: ---> ($archive)';
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
} else { } else {
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls
let archive = $'($dist)/($releaseStem).zip' let archive = $'($dist)/($releaseStem).zip'
7z a $archive ...(glob *) 7z a $archive *
print $'archive: ---> ($archive)';
let pkg = (ls -f $archive | get name) let pkg = (ls -f $archive | get name)
if not ($pkg | is-empty) { if not ($pkg | is-empty) {
# Workaround for https://github.com/softprops/action-gh-release/issues/280 echo $"archive=($pkg | get 0)" | save --append $env.GITHUB_OUTPUT
let archive = ($pkg | get 0 | str replace --all '\' '/')
print $'archive: ---> ($archive)'
echo $"archive=($archive)" | save --append $env.GITHUB_OUTPUT
} }
} }
} }
def 'cargo-build-nu' [] { def 'cargo-build-nu' [ options: string ] {
if $os == 'windows-latest' { if ($options | str trim | is-empty) {
cargo build --release --all --target $target
} else {
cargo build --release --all --target $target --features=static-link-openssl cargo build --release --all --target $target --features=static-link-openssl
} else {
cargo build --release --all --target $target --features=static-link-openssl $options
} }
} }
# Print a horizontal line marker # Print a horizontal line marker
def 'hr-line' [ def 'hr-line' [
--blank-line(-b) --blank-line(-b): bool
] { ] {
print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)' print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)'
if $blank_line { char nl } if $blank_line { char nl }

View File

@ -14,17 +14,15 @@ defaults:
shell: bash shell: bash
jobs: jobs:
standard: all:
name: Std name: All
strategy: strategy:
fail-fast: false
matrix: matrix:
target: target:
- aarch64-apple-darwin - aarch64-apple-darwin
- x86_64-apple-darwin - x86_64-apple-darwin
- x86_64-pc-windows-msvc - x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
- x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl - x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu - aarch64-unknown-linux-gnu
@ -34,51 +32,52 @@ jobs:
include: include:
- target: aarch64-apple-darwin - target: aarch64-apple-darwin
os: macos-latest os: macos-latest
target_rustflags: ''
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
extra: 'bin' extra: 'bin'
os: windows-latest os: windows-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc
extra: msi extra: msi
os: windows-latest os: windows-latest
- target: aarch64-pc-windows-msvc target_rustflags: ''
extra: 'bin'
os: windows-latest
- target: aarch64-pc-windows-msvc
extra: msi
os: windows-latest
- target: x86_64-unknown-linux-gnu - target: x86_64-unknown-linux-gnu
os: ubuntu-20.04 os: ubuntu-20.04
target_rustflags: ''
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
os: ubuntu-20.04 os: ubuntu-20.04
target_rustflags: ''
- target: aarch64-unknown-linux-gnu - target: aarch64-unknown-linux-gnu
os: ubuntu-20.04 os: ubuntu-20.04
target_rustflags: ''
- target: armv7-unknown-linux-gnueabihf - target: armv7-unknown-linux-gnueabihf
os: ubuntu-20.04 os: ubuntu-20.04
target_rustflags: ''
- target: riscv64gc-unknown-linux-gnu - target: riscv64gc-unknown-linux-gnu
os: ubuntu-latest os: ubuntu-20.04
target_rustflags: ''
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
steps: steps:
- uses: actions/checkout@v4.1.6 - uses: actions/checkout@v3
- name: Update Rust Toolchain Target - name: Update Rust Toolchain Target
run: | run: |
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
- name: Setup Rust toolchain - name: Setup Rust toolchain and cache
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rust-lang/setup-rust-toolchain@v1.4.3
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
with:
cache: false
rustflags: ''
- name: Setup Nushell - name: Setup Nushell
uses: hustcer/setup-nu@v3.10 uses: hustcer/setup-nu@v3
with: with:
version: 0.93.0 version: 0.72.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release Nu Binary - name: Release Nu Binary
id: nu id: nu
@ -88,10 +87,11 @@ jobs:
REF: ${{ github.ref }} REF: ${{ github.ref }}
TARGET: ${{ matrix.target }} TARGET: ${{ matrix.target }}
_EXTRA_: ${{ matrix.extra }} _EXTRA_: ${{ matrix.extra }}
TARGET_RUSTFLAGS: ${{ matrix.target_rustflags }}
# REF: https://github.com/marketplace/actions/gh-release # REF: https://github.com/marketplace/actions/gh-release
- name: Publish Archive - name: Publish Archive
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v0.1.13
if: ${{ startsWith(github.ref, 'refs/tags/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
with: with:
draft: true draft: true

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v3
- name: Check spelling - name: Check spelling
uses: crate-ci/typos@v1.21.0 uses: crate-ci/typos@master

View File

@ -7,22 +7,19 @@ on:
inputs: inputs:
tag_name: tag_name:
description: 'Specific tag name' description: 'Specific tag name'
required: true required: true
type: string type: string
jobs: jobs:
winget: winget:
name: Publish winget package name: Publish winget package
runs-on: ubuntu-latest runs-on: windows-latest
steps: steps:
- name: Submit package to Windows Package Manager Community Repository - name: Submit package to Windows Package Manager Community Repository
uses: vedantmgoyal2009/winget-releaser@v2 uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: Nushell.Nushell identifier: Nushell.Nushell
# Exclude all `*-msvc-full.msi` full release files,
# and only the default `*msvc.msi` files will be included
installers-regex: 'msvc\.msi$'
version: ${{ inputs.tag_name || github.event.release.tag_name }} version: ${{ inputs.tag_name || github.event.release.tag_name }}
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }} release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
token: ${{ secrets.NUSHELL_PAT }} token: ${{ secrets.NUSHELL_PAT }}

4
.gitignore vendored
View File

@ -22,9 +22,6 @@ debian/nu/
# VSCode's IDE items # VSCode's IDE items
.vscode/* .vscode/*
# JetBrains' Fleet IDE
.fleet/*
# Visual Studio Extension SourceGear Rust items # Visual Studio Extension SourceGear Rust items
VSWorkspaceSettings.json VSWorkspaceSettings.json
unstable_cargo_features.txt unstable_cargo_features.txt
@ -42,7 +39,6 @@ tarpaulin-report.html
*.rsproj *.rsproj
*.rsproj.user *.rsproj.user
*.sln *.sln
*.code-workspace
# direnv # direnv
.direnv/ .direnv/

12
.typos.toml Normal file
View File

@ -0,0 +1,12 @@
[files]
extend-exclude = ["crates/nu-command/tests/commands/table.rs", "*.tsv", "*.json", "*.txt"]
[default.extend-words]
# Ignore false-positives
nd = "nd"
fo = "fo"
ons = "ons"
ba = "ba"
Plasticos = "Plasticos"
IIF = "IIF"
numer = "numer"

View File

@ -2,25 +2,7 @@
Welcome to Nushell and thank you for considering contributing! Welcome to Nushell and thank you for considering contributing!
## Table of contents ## Review Process
- [Proposing design changes](#proposing-design-changes)
- [Developing](#developing)
- [Setup](#setup)
- [Tests](#tests)
- [Useful commands](#useful-commands)
- [Debugging tips](#debugging-tips)
- [Git etiquette](#git-etiquette)
- [License](#license)
## Other helpful resources
More resources can be found in the nascent [developer documentation](devdocs/README.md) in this repo.
- [Developer FAQ](devdocs/FAQ.md)
- [Platform support policy](devdocs/PLATFORM_SUPPORT.md)
- [Our Rust style](devdocs/rust_style.md)
## Proposing design changes
First of all, before diving into the code, if you want to create a new feature, change something significantly, and especially if the change is user-facing, it is a good practice to first get an approval from the core team before starting to work on it. First of all, before diving into the code, if you want to create a new feature, change something significantly, and especially if the change is user-facing, it is a good practice to first get an approval from the core team before starting to work on it.
This saves both your and our time if we realize the change needs to go another direction before spending time on it. This saves both your and our time if we realize the change needs to go another direction before spending time on it.
@ -51,186 +33,69 @@ cargo build
### Tests ### Tests
It is good practice to cover your changes with a test. Also, try to think about corner cases and various ways how your changes could break. Cover those in the tests as well. It is a good practice to cover your changes with a test. Also, try to think about corner cases and various ways how your changes could break. Cover those in the tests as well.
Tests can be found in different places: Tests can be found in different places:
* `/tests` * `/tests`
* `src/tests`
* command examples * command examples
* crate-specific tests * crate-specific tests
Most of the tests are built upon the `nu-test-support` crate. For testing specific features, such as running Nushell in a REPL mode, we have so called "testbins". For simple tests, you can find `run_test()` and `fail_test()` functions. The most comprehensive test suite we have is the `nu-test-support` crate. For testing specific features, such as running Nushell in a REPL mode, we have so called "testbins". For simple tests, you can find `run_test()` and `fail_test()` functions.
### Useful Commands ### Useful Commands
As Nushell is built using a cargo workspace consisting of multiple crates keep in mind that you may need to pass additional flags compared to how you may be used to it from a single crate project.
Read cargo's documentation for more details: https://doc.rust-lang.org/cargo/reference/workspaces.html
- Build and run Nushell: - Build and run Nushell:
```nushell ```shell
cargo run cargo run
``` ```
- Build and run with dataframe support.
```shell
cargo run --features=dataframe
```
- Run Clippy on Nushell: - Run Clippy on Nushell:
```nushell ```shell
cargo clippy --workspace -- -D warnings -D clippy::unwrap_used cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect
```
or via the `toolkit.nu` command:
```nushell
use toolkit.nu clippy
clippy
``` ```
- Run all tests: - Run all tests:
```nushell ```shell
cargo test --workspace cargo test --workspace
``` ```
or via the `toolkit.nu` command:
```nushell
use toolkit.nu test
test
```
- Run all tests for a specific command - Run all tests for a specific command
```nushell ```shell
cargo test --package nu-cli --test main -- commands::<command_name_here> cargo test --package nu-cli --test main -- commands::<command_name_here>
``` ```
- Check to see if there are code formatting issues - Check to see if there are code formatting issues
```nushell ```shell
cargo fmt --all -- --check cargo fmt --all -- --check
``` ```
or via the `toolkit.nu` command:
```nushell
use toolkit.nu fmt
fmt --check
```
- Format the code in the project - Format the code in the project
```nushell ```shell
cargo fmt --all cargo fmt --all
``` ```
or via the `toolkit.nu` command:
```nushell
use toolkit.nu fmt
fmt
```
- Set up `git` hooks to check formatting and run `clippy` before committing and pushing:
```nushell
use toolkit.nu setup-git-hooks
setup-git-hooks
```
_Unfortunately, this hook isn't available on Windows._
### Debugging Tips ### Debugging Tips
- To view verbose logs when developing, enable the `trace` log level. - To view verbose logs when developing, enable the `trace` log level.
```nushell ```shell
cargo run --release -- --log-level trace cargo run --release -- --log-level trace
``` ```
- To redirect trace logs to a file, enable the `--log-target file` switch. - To redirect trace logs to a file, enable the `--log-target file` switch.
```nushell ```shell
cargo run --release -- --log-level trace --log-target file cargo run --release -- --log-level trace --log-target file
open $"($nu.temp-path)/nu-($nu.pid).log" open $"($nu.temp-path)/nu-($nu.pid).log"
``` ```
## Git etiquette
As nushell thrives on its broad base of volunteer contributors and maintainers with different backgrounds we have a few guidelines for how we best utilize git and GitHub for our contributions. We strive to balance three goals with those recommendations:
1. The **volunteer maintainers and contributors** can easily follow the changes you propose, gauge the impact, and come to help you or make a decision.
2. **You as a contributor** can focus most of your time on improving the quality of the nushell project and contributing your expertise to the code or documentation.
3. Making sure we can trace back *why* decisions were made in the past.
This includes discarded approaches. Also we want to quickly identify regressions and fix when something broke.
### How we merge PRs
In general the maintainers **squash** all changes of your PR into a single commit when merging.
This keeps a clean enough linear history, while not forcing you to conform to a too strict style while iterating in your PR or fixing small problems. As an added benefit the commits on the `main` branch are tied to the discussion that happened in the PR through their `#1234` issue number.
> **Note**
> **Pro advice:** In some circumstances, we can agree on rebase-merging a particularly large but connected PR as a series of atomic commits onto the `main` branch to ensure we can more easily revert or bisect particular aspects.
### A good PR makes a change!
As a result of this PR-centric strategy and the general goal that the reviewers should easily understand your change, the **PR title and description matters** a great deal!
Make sure your description is **concise** but contains all relevant information and context.
This means demonstrating what changes, ideally through nushell code or output **examples**.
Furthermore links to technical documentation or instructions for folks that want to play with your change make the review process much easier.
> **Note**
> Try to follow the suggestions in our PR message template to make sure we can quickly focus on the technical merits and impact on the users.
#### A PR should limit itself to a single functional change or related set of same changes.
Mixing different changes in the same PR will make the review process much harder. A PR might get stuck on one aspect while we would actually like to land another change. Furthermore, if we are forced to revert a change, mixing and matching different aspects makes fixing bugs or regressions much harder.
Thus, please try to **separate out unrelated changes**!
**Don't** mix unrelated refactors with a potentially contested change.
Stylistic fixes and housekeeping can be bundled up into singular PRs.
#### Guidelines for the PR title
The PR title should be concise but contain everything for a contributor to know if they should help out in the review of this particular change.
**DON'T**
- `Update file/in/some/deeply/nested/path.rs`
- Why are you making this change?
- `Fix 2134`
- What has to be fixed?
- Hard to follow when not online on GitHub.
- ``Ignore `~` expansion``
- In what context should this change take effect?
- `[feature] refactor the whole parser and also make nushell indentation-sensitive, upgrade to using Cpython. Let me know what you think!`
- Be concise
- Maybe break up into smaller commits or PRs if the title already appears too long?
**DO**
- Mention the nushell feature or command that is affected.
- ``Fix URL parsing in `http get` (issue #1234)``
- You can mention the issue number if other context is there.
- In general, mention all related issues in the description to crosslink (e.g. `Fixes #1234`, `Closes #6789`)
- For internal changes mention the area or symbols affected if it helps to clarify
- ``Factor out `quote_string()` from parser to reuse in `explore` ``
### Review process / Merge conflicts
> **Note**
> Keep in mind that the maintainers are volunteers that need to allocate their attention to several different areas and active PRs. We will try to get back to you as soon as possible.
You can help us to make the review process a smooth experience:
- Testing:
- We generally review in detail after all the tests pass. Let us know if there is a problem you want to discuss to fix a test failure or forces us to accept a breaking change.
- If you fix a bug, it is highly recommended that you add a test that reproduces the original issue/panic in a minimal form.
- In general, added tests help us to understand which assumptions go into a particular addition/change.
- Try to also test corner cases where those assumptions might break. This can be more valuable than simply adding many similar tests.
- Commit history inside a PR during code review:
- Good **atomic commits** can help follow larger changes, but we are not pedantic.
- We don't shame fixup commits while you try to figure out a problem. They can help others see what you tried and what didn't work. (see our [squash policy](#how-we-merge-prs))
- During active review constant **force pushing** just to amend changes can be confusing!
- GitHub's UI presents reviewers with less options to compare diffs
- fetched branches for experimentation become invalid!
- the notification a maintainer receives has a low signal-to-noise ratio
- Git pros *can* use their judgement to rebase/squash to clean up the history *if it aids the understanding* of a larger change during review
- Merge conflicts:
- In general you should take care of resolving merge conflicts.
- Use your judgement whether to `git merge main` or to `git rebase main`
- Choose what simplifies having confidence in the conflict resolution and the review. **Merge commits in your branch are OK** in the squash model.
- Feel free to notify your reviewers or affected PR authors if your change might cause larger conflicts with another change.
- During the rollup of multiple PRs, we may choose to resolve merge conflicts and CI failures ourselves. (Allow maintainers to push to your branch to enable us to do this quickly.)
## License
We use the [MIT License](https://github.com/nushell/nushell/blob/main/LICENSE) in all of our Nushell projects. If you are including or referencing a crate that uses the [GPL License](https://www.gnu.org/licenses/gpl-3.0.en.html#license-text) unfortunately we will not be able to accept your PR.

5230
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
[package] [package]
authors = ["The Nushell Project Developers"] authors = ["The Nushell Project Developers"]
build = "scripts/build.rs"
default-run = "nu" default-run = "nu"
description = "A new type of shell" description = "A new type of shell"
documentation = "https://www.nushell.sh/book/" documentation = "https://www.nushell.sh/book/"
@ -10,8 +9,8 @@ homepage = "https://www.nushell.sh"
license = "MIT" license = "MIT"
name = "nu" name = "nu"
repository = "https://github.com/nushell/nushell" repository = "https://github.com/nushell/nushell"
rust-version = "1.77.2" rust-version = "1.60"
version = "0.94.0" version = "0.77.1"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -24,251 +23,111 @@ pkg-fmt = "zip"
[workspace] [workspace]
members = [ members = [
"crates/nu-cli", "crates/nu-cli",
"crates/nu-engine", "crates/nu-engine",
"crates/nu-parser", "crates/nu-parser",
"crates/nu-system", "crates/nu-system",
"crates/nu-cmd-base", "crates/nu-cmd-lang",
"crates/nu-cmd-extra", "crates/nu-command",
"crates/nu-cmd-lang", "crates/nu-protocol",
"crates/nu-cmd-plugin", "crates/nu-plugin",
"crates/nu-command", "crates/nu_plugin_inc",
"crates/nu-color-config", "crates/nu_plugin_gstat",
"crates/nu-explore", "crates/nu_plugin_example",
"crates/nu-json", "crates/nu_plugin_query",
"crates/nu-lsp", "crates/nu_plugin_custom_values",
"crates/nu-pretty-hex", "crates/nu_plugin_formats",
"crates/nu-protocol", "crates/nu-utils",
"crates/nu-plugin",
"crates/nu-plugin-core",
"crates/nu-plugin-engine",
"crates/nu-plugin-protocol",
"crates/nu-plugin-test-support",
"crates/nu_plugin_inc",
"crates/nu_plugin_gstat",
"crates/nu_plugin_example",
"crates/nu_plugin_query",
"crates/nu_plugin_custom_values",
"crates/nu_plugin_formats",
"crates/nu_plugin_polars",
"crates/nu_plugin_stress_internals",
"crates/nu-std",
"crates/nu-table",
"crates/nu-term-grid",
"crates/nu-test-support",
"crates/nu-utils",
"crates/nuon",
] ]
[workspace.dependencies]
alphanumeric-sort = "1.5"
ansi-str = "0.8"
anyhow = "1.0.82"
base64 = "0.22.1"
bracoxide = "0.1.2"
brotli = "5.0"
byteorder = "1.5"
bytesize = "1.3"
calamine = "0.24.0"
chardetng = "0.1.17"
chrono = { default-features = false, version = "0.4.34" }
chrono-humanize = "0.2.3"
chrono-tz = "0.8"
crossbeam-channel = "0.5.8"
crossterm = "0.27"
csv = "1.3"
ctrlc = "3.4"
dialoguer = { default-features = false, version = "0.11" }
digest = { default-features = false, version = "0.10" }
dirs-next = "2.0"
dtparse = "2.0"
encoding_rs = "0.8"
fancy-regex = "0.13"
filesize = "0.2"
filetime = "0.2"
fs_extra = "1.3"
fuzzy-matcher = "0.3"
hamcrest2 = "0.3"
heck = "0.5.0"
human-date-parser = "0.1.1"
indexmap = "2.2"
indicatif = "0.17"
interprocess = "2.1.0"
is_executable = "1.0"
itertools = "0.12"
libc = "0.2"
libproc = "0.14"
log = "0.4"
lru = "0.12"
lscolors = { version = "0.17", default-features = false }
lsp-server = "0.7.5"
lsp-types = "0.95.0"
mach2 = "0.4"
md5 = { version = "0.10", package = "md-5" }
miette = "7.2"
mime = "0.3"
mime_guess = "2.0"
mockito = { version = "1.4", default-features = false }
native-tls = "0.2"
nix = { version = "0.28", default-features = false }
notify-debouncer-full = { version = "0.3", default-features = false }
nu-ansi-term = "0.50.0"
num-format = "0.4"
num-traits = "0.2"
omnipath = "0.1"
once_cell = "1.18"
open = "5.1"
os_pipe = { version = "1.1", features = ["io_safety"] }
pathdiff = "0.2"
percent-encoding = "2"
pretty_assertions = "1.4"
print-positions = "0.6"
procfs = "0.16.0"
pwd = "1.3"
quick-xml = "0.31.0"
quickcheck = "1.0"
quickcheck_macros = "1.0"
rand = "0.8"
ratatui = "0.26"
rayon = "1.10"
reedline = "0.32.0"
regex = "1.9.5"
rmp = "0.8"
rmp-serde = "1.3"
ropey = "1.6.1"
roxmltree = "0.19"
rstest = { version = "0.18", default-features = false }
rusqlite = "0.31"
rust-embed = "8.4.0"
same-file = "1.0"
serde = { version = "1.0", default-features = false }
serde_json = "1.0"
serde_urlencoded = "0.7.1"
serde_yaml = "0.9"
sha2 = "0.10"
strip-ansi-escapes = "0.2.0"
sysinfo = "0.30"
tabled = { version = "0.14.0", default-features = false }
tempfile = "3.10"
terminal_size = "0.3"
titlecase = "2.0"
toml = "0.8"
trash = "3.3"
umask = "2.1"
unicode-segmentation = "1.11"
unicode-width = "0.1"
ureq = { version = "2.9", default-features = false }
url = "2.2"
uu_cp = "0.0.25"
uu_mkdir = "0.0.25"
uu_mktemp = "0.0.25"
uu_mv = "0.0.25"
uu_whoami = "0.0.25"
uu_uname = "0.0.25"
uucore = "0.0.25"
uuid = "1.8.0"
v_htmlescape = "0.15.0"
wax = "0.6"
which = "6.0.0"
windows = "0.54"
winreg = "0.52"
[dependencies] [dependencies]
nu-cli = { path = "./crates/nu-cli", version = "0.94.0" } chrono = { version = "0.4.23", features = ["serde"] }
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.94.0" } crossterm = "0.24.0"
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.94.0" } ctrlc = "3.2.1"
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.94.0", optional = true } log = "0.4"
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.94.0" } miette = { version = "5.5.0", features = ["fancy-no-backtrace"] }
nu-command = { path = "./crates/nu-command", version = "0.94.0" } nu-cli = { path = "./crates/nu-cli", version = "0.77.1" }
nu-engine = { path = "./crates/nu-engine", version = "0.94.0" } nu-color-config = { path = "./crates/nu-color-config", version = "0.77.1" }
nu-explore = { path = "./crates/nu-explore", version = "0.94.0" } nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.77.1" }
nu-lsp = { path = "./crates/nu-lsp/", version = "0.94.0" } nu-command = { path = "./crates/nu-command", version = "0.77.1" }
nu-parser = { path = "./crates/nu-parser", version = "0.94.0" } nu-engine = { path = "./crates/nu-engine", version = "0.77.1" }
nu-path = { path = "./crates/nu-path", version = "0.94.0" } nu-json = { path = "./crates/nu-json", version = "0.77.1" }
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.94.0" } nu-parser = { path = "./crates/nu-parser", version = "0.77.1" }
nu-protocol = { path = "./crates/nu-protocol", version = "0.94.0" } nu-path = { path = "./crates/nu-path", version = "0.77.1" }
nu-std = { path = "./crates/nu-std", version = "0.94.0" } nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.77.1" }
nu-system = { path = "./crates/nu-system", version = "0.94.0" } nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.77.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.94.0" } nu-protocol = { path = "./crates/nu-protocol", version = "0.77.1" }
nu-system = { path = "./crates/nu-system", version = "0.77.1" }
nu-table = { path = "./crates/nu-table", version = "0.77.1" }
nu-term-grid = { path = "./crates/nu-term-grid", version = "0.77.1" }
nu-utils = { path = "./crates/nu-utils", version = "0.77.1" }
reedline = { workspace = true, features = ["bashisms", "sqlite"] } nu-ansi-term = "0.47.0"
reedline = { version = "0.17.0", features = ["bashisms", "sqlite"] }
crossterm = { workspace = true } rayon = "1.7.0"
ctrlc = { workspace = true } is_executable = "1.0.1"
log = { workspace = true } simplelog = "0.12.0"
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] } time = "0.3.12"
mimalloc = { version = "0.1.42", default-features = false, optional = true }
serde_json = { workspace = true }
simplelog = "0.12"
time = "0.3"
[target.'cfg(not(target_os = "windows"))'.dependencies] [target.'cfg(not(target_os = "windows"))'.dependencies]
# Our dependencies don't use OpenSSL on Windows # Our dependencies don't use OpenSSL on Windows
openssl = { version = "0.10", features = ["vendored"], optional = true } openssl = { version = "0.10.38", features = ["vendored"], optional = true }
signal-hook = { version = "0.3.14", default-features = false }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
winresource = "0.1" winres = "0.1"
[target.'cfg(target_family = "unix")'.dependencies] [target.'cfg(target_family = "unix")'.dependencies]
nix = { workspace = true, default-features = false, features = [ nix = { version = "0.26", default-features = false, features = [
"signal", "signal",
"process", "process",
"fs", "fs",
"term", "term",
] } ] }
atty = "0.2"
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.94.0" } nu-test-support = { path = "./crates/nu-test-support", version = "0.77.1" }
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.94.0" } tempfile = "3.4.0"
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.94.0" } assert_cmd = "2.0.2"
assert_cmd = "2.0" criterion = "0.4"
dirs-next = { workspace = true } pretty_assertions = "1.0.0"
tango-bench = "0.5" serial_test = "1.0.0"
pretty_assertions = { workspace = true } hamcrest2 = "0.3.0"
rstest = { workspace = true, default-features = false } rstest = { version = "0.16.0", default-features = false }
serial_test = "3.1" itertools = "0.10.3"
tempfile = { workspace = true }
[features] [features]
plugin = [ plugin = [
"nu-plugin-engine", "nu-plugin",
"nu-cmd-plugin", "nu-cli/plugin",
"nu-cli/plugin", "nu-parser/plugin",
"nu-parser/plugin", "nu-command/plugin",
"nu-command/plugin", "nu-protocol/plugin",
"nu-protocol/plugin", "nu-engine/plugin",
"nu-engine/plugin",
]
default = ["default-no-clipboard", "system-clipboard"]
# Enables convenient omitting of the system-clipboard feature, as it leads to problems in ci on linux
# See https://github.com/nushell/nushell/pull/11535
default-no-clipboard = [
"plugin",
"which-support",
"trash-support",
"sqlite",
"mimalloc",
] ]
# extra used to be more useful but now it's the same as default. Leaving it in for backcompat with existing build scripts
extra = ["default"]
default = ["plugin", "which-support", "trash-support", "sqlite"]
stable = ["default"] stable = ["default"]
# NOTE: individual features are also passed to `nu-cmd-lang` that uses them to generate the feature matrix in the `version` command wasi = []
# Enable to statically link OpenSSL (perl is required, to build OpenSSL https://docs.rs/openssl/latest/openssl/); # Enable to statically link OpenSSL; otherwise the system version will be used. Not enabled by default because it takes a while to build
# otherwise the system version will be used. Not enabled by default because it takes a while to build static-link-openssl = ["dep:openssl"]
static-link-openssl = ["dep:openssl", "nu-cmd-lang/static-link-openssl"]
mimalloc = ["nu-cmd-lang/mimalloc", "dep:mimalloc"]
system-clipboard = [
"reedline/system_clipboard",
"nu-cli/system-clipboard",
"nu-cmd-lang/system-clipboard",
]
# Stable (Default) # Stable (Default)
which-support = ["nu-command/which-support", "nu-cmd-lang/which-support"] which-support = ["nu-command/which-support"]
trash-support = ["nu-command/trash-support", "nu-cmd-lang/trash-support"] trash-support = ["nu-command/trash-support"]
# Extra
# Dataframe feature for nushell
dataframe = ["nu-command/dataframe"]
# SQLite commands for nushell # SQLite commands for nushell
sqlite = ["nu-command/sqlite", "nu-cmd-lang/sqlite"] sqlite = ["nu-command/sqlite"]
[profile.release] [profile.release]
opt-level = "s" # Optimize for size opt-level = "s" # Optimize for size
@ -298,11 +157,12 @@ bench = false
# To use a development version of a dependency please use a global override here # To use a development version of a dependency please use a global override here
# changing versions in each sub-crate of the workspace is tedious # changing versions in each sub-crate of the workspace is tedious
[patch.crates-io] [patch.crates-io]
# reedline = { git = "https://github.com/nushell/reedline", branch = "main" } # reedline = { git = "https://github.com/nushell/reedline.git", branch = "main" }
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
# Criterion benchmarking setup
# Run all benchmarks with `cargo bench` # Run all benchmarks with `cargo bench`
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse` # Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
[[bench]] [[bench]]
name = "benchmarks" name = "benchmarks"
harness = false harness = false

View File

@ -1,18 +1,9 @@
# Configuration for cross-rs: https://github.com/cross-rs/cross # Configuration for cross-rs: https://github.com/cross-rs/cross
# Run cross-rs like this: # Run cross-rs like this:
# cross build --target aarch64-unknown-linux-gnu --release # cross build --target aarch64-unknown-linux-musl --release
# or
# cross build --target aarch64-unknown-linux-musl --release --features=static-link-openssl
[target.aarch64-unknown-linux-gnu] [target.aarch64-unknown-linux-gnu]
pre-build = [ dockerfile = "./docker/cross-rs/aarch64-unknown-linux-gnu.dockerfile"
"dpkg --add-architecture $CROSS_DEB_ARCH",
"apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH clang"
]
# NOTE: for musl you will need to build with --features=static-link-openssl
[target.aarch64-unknown-linux-musl] [target.aarch64-unknown-linux-musl]
pre-build = [ dockerfile = "./docker/cross-rs/aarch64-unknown-linux-musl.dockerfile"
"dpkg --add-architecture $CROSS_DEB_ARCH",
"apt-get update && apt-get install --assume-yes clang"
]

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,28 +1,28 @@
# Nushell <!-- omit in toc --> # Nushell <!-- omit in toc -->
[![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu) [![Crates.io](https://img.shields.io/crates/v/nu.svg)](https://crates.io/crates/nu)
[![Build Status](https://img.shields.io/github/actions/workflow/status/nushell/nushell/ci.yml?branch=main)](https://github.com/nushell/nushell/actions) [![Build Status](https://img.shields.io/github/actions/workflow/status/nushell/nushell/ci.yml?branch=main)](https://github.com/nushell/nushell/actions)
[![Nightly Build](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml/badge.svg)](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml)
[![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn) [![Discord](https://img.shields.io/discord/601130461678272522.svg?logo=discord)](https://discord.gg/NtAbbGn)
[![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363) [![The Changelog #363](https://img.shields.io/badge/The%20Changelog-%23363-61c192.svg)](https://changelog.com/podcast/363)
[![@nu_shell](https://img.shields.io/badge/twitter-@nu_shell-1DA1F3?style=flat-square)](https://twitter.com/nu_shell) [![@nu_shell](https://img.shields.io/badge/twitter-@nu_shell-1DA1F3?style=flat-square)](https://twitter.com/nu_shell)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell)](https://github.com/nushell/nushell/graphs/commit-activity) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/nushell/nushell)](https://github.com/nushell/nushell/graphs/commit-activity)
[![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell)](https://github.com/nushell/nushell/graphs/contributors) [![GitHub contributors](https://img.shields.io/github/contributors/nushell/nushell)](https://github.com/nushell/nushell/graphs/contributors)
[![codecov](https://codecov.io/gh/nushell/nushell/branch/main/graph/badge.svg?token=JheS8qu2II)](https://codecov.io/gh/nushell/nushell)
A new type of shell. A new type of shell.
![Example of nushell](assets/nushell-autocomplete6.gif "Example of nushell") ![Example of nushell](images/nushell-autocomplete6.gif "Example of nushell")
## Table of Contents <!-- omit in toc --> ## Table of Contents <!-- omit in toc -->
- [Status](#status) - [Status](#status)
- [Learning About Nu](#learning-about-nu) - [Learning About Nu](#learning-about-nu)
- [Installation](#installation) - [Installation](#installation)
- [Configuration](#configuration)
- [Philosophy](#philosophy) - [Philosophy](#philosophy)
- [Pipelines](#pipelines) - [Pipelines](#pipelines)
- [Opening files](#opening-files) - [Opening files](#opening-files)
- [Plugins](#plugins) - [Plugins](#plugins)
- [Goals](#goals) - [Goals](#goals)
- [Progress](#progress)
- [Officially Supported By](#officially-supported-by) - [Officially Supported By](#officially-supported-by)
- [Contributing](#contributing) - [Contributing](#contributing)
- [License](#license) - [License](#license)
@ -54,23 +54,6 @@ Detailed installation instructions can be found in the [installation chapter of
[![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/nushell.svg)](https://repology.org/project/nushell/versions)
For details about which platforms the Nushell team actively supports, see [our platform support policy](devdocs/PLATFORM_SUPPORT.md).
## Configuration
The default configurations can be found at [sample_config](crates/nu-utils/src/sample_config)
which are the configuration files one gets when they startup Nushell for the first time.
It sets all of the default configuration to run Nushell. From here one can
then customize this file for their specific needs.
To see where *config.nu* is located on your system simply type this command.
```rust
$nu.config-path
```
Please see our [book](https://www.nushell.sh) for all of the Nushell documentation.
## Philosophy ## Philosophy
@ -192,14 +175,13 @@ These binaries interact with nu via a simple JSON-RPC protocol where the command
If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout. If the plugin is a filter, data streams to it one element at a time, and it can stream data back in return via stdin/stdout.
If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases. If the plugin is a sink, it is given the full vector of final data and is given free reign over stdin/stdout to use as it pleases.
The [awesome-nu repo](https://github.com/nushell/awesome-nu#plugins) lists a variety of nu-plugins while the [showcase repo](https://github.com/nushell/showcase) *shows* off informative blog posts that have been written about Nushell along with videos that highlight technical The [awesome-nu repo](https://github.com/nushell/awesome-nu#plugins) lists a variety of nu-plugins.
topics that have been presented.
## Goals ## Goals
Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals. Nu adheres closely to a set of goals that make up its design philosophy. As features are added, they are checked against these goals.
- First and foremost, Nu is cross-platform. Commands and techniques should work across platforms and Nu has [first-class support for Windows, macOS, and Linux](devdocs/PLATFORM_SUPPORT.md). - First and foremost, Nu is cross-platform. Commands and techniques should work across platforms and Nu has first-class support for Windows, macOS, and Linux.
- Nu ensures compatibility with existing platform-specific executables. - Nu ensures compatibility with existing platform-specific executables.
@ -209,6 +191,27 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat
- Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state. - Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state.
## Progress
Nu is under heavy development and will naturally change as it matures. The chart below isn't meant to be exhaustive, but it helps give an idea for some of the areas of development and their relative maturity:
| Features | Not started | Prototype | MVP | Preview | Mature | Notes |
| ------------- | :---------: | :-------: | :-: | :-----: | :----: | -------------------------------------------------------------------- |
| Aliases | | | | X | | Aliases allow for shortening large commands, while passing flags |
| Notebook | | X | | | | Initial jupyter support, but it loses state and lacks features |
| File ops | | | | X | | cp, mv, rm, mkdir have some support, but lacking others |
| Environment | | | | X | | Temporary environment and scoped environment variables |
| Shells | | | | X | | Basic value and file shells, but no opt-in/opt-out for commands |
| Protocol | | | | X | | Streaming protocol is serviceable |
| Plugins | | | X | | | Plugins work on one row at a time, lack batching and expression eval |
| Errors | | | | X | | Error reporting works, but could use usability polish |
| Documentation | | | X | | | Book updated to latest release, including usage examples |
| Paging | | | | X | | Textview has paging, but we'd like paging for tables |
| Functions | | | | X | | Functions and aliases are supported |
| Variables | | | | X | | Nu supports variables and environment variables |
| Completions | | | | X | | Completions for filepaths |
| Type-checking | | | | x | | Commands check basic types, and input/output types |
## Officially Supported By ## Officially Supported By
Please submit an issue or PR to be added to this list. Please submit an issue or PR to be added to this list.
@ -218,17 +221,13 @@ Please submit an issue or PR to be added to this list.
- [oh-my-posh](https://ohmyposh.dev) - [oh-my-posh](https://ohmyposh.dev)
- [Couchbase Shell](https://couchbase.sh) - [Couchbase Shell](https://couchbase.sh)
- [virtualenv](https://github.com/pypa/virtualenv) - [virtualenv](https://github.com/pypa/virtualenv)
- [atuin](https://github.com/ellie/atuin)
- [clap](https://github.com/clap-rs/clap/tree/master/clap_complete_nushell)
- [Dorothy](http://github.com/bevry/dorothy)
- [Direnv](https://github.com/direnv/direnv/blob/master/docs/hook.md#nushell)
## Contributing ## Contributing
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed! See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
<a href="https://github.com/nushell/nushell/graphs/contributors"> <a href="https://github.com/nushell/nushell/graphs/contributors">
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750" /> <img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=500" />
</a> </a>
## License ## License

3
README.release.txt Normal file
View File

@ -0,0 +1,3 @@
To use Nu plugins, use the register command to tell Nu where to find the plugin. For example:
> register ./nu_plugin_query

View File

@ -1,6 +1,6 @@
# Divan benchmarks # Criterion benchmarks
These are benchmarks using [Divan](https://github.com/nvzqz/divan), a microbenchmarking tool for Rust. These are benchmarks using [Criterion](https://github.com/bheisler/criterion.rs), a microbenchmarking tool for Rust.
Run all benchmarks with `cargo bench` Run all benchmarks with `cargo bench`

View File

@ -1,489 +1,191 @@
use nu_cli::{eval_source, evaluate_commands}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
use nu_plugin_core::{Encoder, EncodingType}; use nu_cli::eval_source;
use nu_plugin_protocol::{PluginCallResponse, PluginOutput}; use nu_parser::parse;
use nu_plugin::{EncodingType, PluginResponse};
use nu_protocol::{ use nu_protocol::{PipelineData, Span, Value};
engine::{EngineState, Stack},
PipelineData, Span, Spanned, Value,
};
use nu_std::load_standard_library;
use nu_utils::{get_default_config, get_default_env}; use nu_utils::{get_default_config, get_default_env};
use std::rc::Rc;
use std::hint::black_box; // FIXME: All benchmarks live in this 1 file to speed up build times when benchmarking.
// When the *_benchmarks functions were in different files, `cargo bench` would build
// an executable for every single one - incredibly slowly. Would be nice to figure out
// a way to split things up again.
use tango_bench::{benchmark_fn, tango_benchmarks, tango_main, IntoBenchmarks}; fn parser_benchmarks(c: &mut Criterion) {
let mut engine_state = nu_command::create_default_context();
// parsing config.nu breaks without PWD set
engine_state.add_env_var(
"PWD".into(),
Value::string("/some/dir".to_string(), Span::test_data()),
);
fn load_bench_commands() -> EngineState { let default_env = get_default_env().as_bytes();
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) c.bench_function("parse_default_env_file", |b| {
b.iter_batched(
|| nu_protocol::engine::StateWorkingSet::new(&engine_state),
|mut working_set| parse(&mut working_set, None, default_env, false, &[]),
BatchSize::SmallInput,
)
});
let default_config = get_default_config().as_bytes();
c.bench_function("parse_default_config_file", |b| {
b.iter_batched(
|| nu_protocol::engine::StateWorkingSet::new(&engine_state),
|mut working_set| parse(&mut working_set, None, default_config, false, &[]),
BatchSize::SmallInput,
)
});
c.bench_function("eval default_env.nu", |b| {
b.iter(|| {
let mut engine_state = nu_command::create_default_context();
let mut stack = nu_protocol::engine::Stack::new();
eval_source(
&mut engine_state,
&mut stack,
get_default_env().as_bytes(),
"default_env.nu",
PipelineData::empty(),
false,
)
})
});
c.bench_function("eval default_config.nu", |b| {
b.iter(|| {
let mut engine_state = nu_command::create_default_context();
// parsing config.nu breaks without PWD set
engine_state.add_env_var(
"PWD".into(),
Value::string("/some/dir".to_string(), Span::test_data()),
);
let mut stack = nu_protocol::engine::Stack::new();
eval_source(
&mut engine_state,
&mut stack,
get_default_config().as_bytes(),
"default_config.nu",
PipelineData::empty(),
false,
)
})
});
} }
fn setup_engine() -> EngineState { fn eval_benchmarks(c: &mut Criterion) {
let mut engine_state = load_bench_commands(); c.bench_function("eval default_env.nu", |b| {
let cwd = std::env::current_dir() b.iter(|| {
.unwrap() let mut engine_state = nu_command::create_default_context();
.into_os_string() let mut stack = nu_protocol::engine::Stack::new();
.into_string() eval_source(
.unwrap(); &mut engine_state,
&mut stack,
get_default_env().as_bytes(),
"default_env.nu",
PipelineData::empty(),
false,
)
})
});
// parsing config.nu breaks without PWD set, so set a valid path c.bench_function("eval default_config.nu", |b| {
engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data())); b.iter(|| {
let mut engine_state = nu_command::create_default_context();
engine_state.generate_nu_constant(); // parsing config.nu breaks without PWD set
engine_state.add_env_var(
engine_state "PWD".into(),
} Value::string("/some/dir".to_string(), Span::test_data()),
);
fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { let mut stack = nu_protocol::engine::Stack::new();
let mut engine = setup_engine(); eval_source(
let commands = Spanned { &mut engine_state,
span: Span::unknown(), &mut stack,
item: command.to_string(), get_default_config().as_bytes(),
}; "default_config.nu",
PipelineData::empty(),
let mut stack = Stack::new(); false,
evaluate_commands( )
&commands, })
&mut engine, });
&mut stack,
PipelineData::empty(),
None,
false,
)
.unwrap();
(stack, engine)
} }
// generate a new table data with `row_cnt` rows, `col_cnt` columns. // generate a new table data with `row_cnt` rows, `col_cnt` columns.
fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value { fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value {
let record = Value::test_record( let columns: Vec<String> = (0..col_cnt).map(|x| format!("col_{x}")).collect();
(0..col_cnt) let vals: Vec<Value> = (0..col_cnt as i64).map(Value::test_int).collect();
.map(|x| (format!("col_{x}"), Value::test_int(x as i64)))
Value::List {
vals: (0..row_cnt)
.map(|_| Value::test_record(columns.clone(), vals.clone()))
.collect(), .collect(),
); span: Span::test_data(),
}
Value::list(vec![record; row_cnt], Span::test_data())
} }
fn bench_command( fn encoding_benchmarks(c: &mut Criterion) {
name: &str, let mut group = c.benchmark_group("Encoding");
command: &str, let test_cnt_pairs = [
stack: Stack, (100, 5),
engine: EngineState, (100, 10),
) -> impl IntoBenchmarks { (100, 15),
let commands = Spanned { (1000, 5),
span: Span::unknown(), (1000, 10),
item: command.to_string(), (1000, 15),
}; (10000, 5),
[benchmark_fn(name, move |b| { (10000, 10),
let commands = commands.clone(); (10000, 15),
let stack = stack.clone(); ];
let engine = engine.clone(); for (row_cnt, col_cnt) in test_cnt_pairs.into_iter() {
b.iter(move || { for fmt in ["json", "msgpack"] {
let mut stack = stack.clone(); group.bench_function(&format!("{fmt} encode {row_cnt} * {col_cnt}"), |b| {
let mut engine = engine.clone(); let mut res = vec![];
#[allow(clippy::unit_arg)] let test_data =
black_box( PluginResponse::Value(Box::new(encoding_test_data(row_cnt, col_cnt)));
evaluate_commands( let encoder = EncodingType::try_from_bytes(fmt.as_bytes()).unwrap();
&commands, b.iter(|| encoder.encode_response(&test_data, &mut res))
&mut engine, });
&mut stack,
PipelineData::empty(),
None,
false,
)
.unwrap(),
);
})
})]
}
fn bench_eval_source(
name: &str,
fname: String,
source: Vec<u8>,
stack: Stack,
engine: EngineState,
) -> impl IntoBenchmarks {
[benchmark_fn(name, move |b| {
let stack = stack.clone();
let engine = engine.clone();
let fname = fname.clone();
let source = source.clone();
b.iter(move || {
let mut stack = stack.clone();
let mut engine = engine.clone();
let fname: &str = &fname.clone();
let source: &[u8] = &source.clone();
black_box(eval_source(
&mut engine,
&mut stack,
source,
fname,
PipelineData::empty(),
false,
));
})
})]
}
/// Load the standard library into the engine.
fn bench_load_standard_lib() -> impl IntoBenchmarks {
[benchmark_fn("load_standard_lib", move |b| {
let engine = setup_engine();
b.iter(move || {
let mut engine = engine.clone();
load_standard_library(&mut engine)
})
})]
}
fn create_flat_record_string(n: i32) -> String {
let mut s = String::from("let record = {");
for i in 0..n {
s.push_str(&format!("col_{}: {}", i, i));
if i < n - 1 {
s.push_str(", ");
} }
} }
s.push('}'); group.finish();
s
} }
fn create_nested_record_string(depth: i32) -> String { fn decoding_benchmarks(c: &mut Criterion) {
let mut s = String::from("let record = {"); let mut group = c.benchmark_group("Decoding");
for _ in 0..depth { let test_cnt_pairs = [
s.push_str("col: {"); (100, 5),
} (100, 10),
s.push_str("col_final: 0"); (100, 15),
for _ in 0..depth { (1000, 5),
s.push('}'); (1000, 10),
} (1000, 15),
s.push('}'); (10000, 5),
s (10000, 10),
} (10000, 15),
];
fn create_example_table_nrows(n: i32) -> String { for (row_cnt, col_cnt) in test_cnt_pairs.into_iter() {
let mut s = String::from("let table = [[foo bar baz]; "); for fmt in ["json", "msgpack"] {
for i in 0..n { group.bench_function(&format!("{fmt} decode for {row_cnt} * {col_cnt}"), |b| {
s.push_str(&format!("[0, 1, {i}]")); let mut res = vec![];
if i < n - 1 { let test_data =
s.push_str(", "); PluginResponse::Value(Box::new(encoding_test_data(row_cnt, col_cnt)));
let encoder = EncodingType::try_from_bytes(fmt.as_bytes()).unwrap();
encoder.encode_response(&test_data, &mut res).unwrap();
let mut binary_data = std::io::Cursor::new(res);
b.iter(|| {
binary_data.set_position(0);
encoder.decode_response(&mut binary_data)
})
});
} }
} }
s.push(']'); group.finish();
s
} }
fn bench_record_create(n: i32) -> impl IntoBenchmarks { criterion_group!(
bench_command( benches,
&format!("record_create_{n}"), parser_benchmarks,
&create_flat_record_string(n), eval_benchmarks,
Stack::new(), encoding_benchmarks,
setup_engine(), decoding_benchmarks
)
}
fn bench_record_flat_access(n: i32) -> impl IntoBenchmarks {
let setup_command = create_flat_record_string(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("record_flat_access_{n}"),
"$record.col_0 | ignore",
stack,
engine,
)
}
fn bench_record_nested_access(n: i32) -> impl IntoBenchmarks {
let setup_command = create_nested_record_string(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
let nested_access = ".col".repeat(n as usize);
bench_command(
&format!("record_nested_access_{n}"),
&format!("$record{} | ignore", nested_access),
stack,
engine,
)
}
fn bench_table_create(n: i32) -> impl IntoBenchmarks {
bench_command(
&format!("table_create_{n}"),
&create_example_table_nrows(n),
Stack::new(),
setup_engine(),
)
}
fn bench_table_get(n: i32) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("table_get_{n}"),
"$table | get bar | math sum | ignore",
stack,
engine,
)
}
fn bench_table_select(n: i32) -> impl IntoBenchmarks {
let setup_command = create_example_table_nrows(n);
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
bench_command(
&format!("table_select_{n}"),
"$table | select foo baz | ignore",
stack,
engine,
)
}
fn bench_eval_interleave(n: i32) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_interleave_{n}"),
&format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
stack,
engine,
)
}
fn bench_eval_interleave_with_ctrlc(n: i32) -> impl IntoBenchmarks {
let mut engine = setup_engine();
engine.ctrlc = Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
false,
)));
let stack = Stack::new();
bench_command(
&format!("eval_interleave_with_ctrlc_{n}"),
&format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
stack,
engine,
)
}
fn bench_eval_for(n: i32) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_for_{n}"),
&format!("(for $x in (1..{n}) {{ 1 }}) | ignore"),
stack,
engine,
)
}
fn bench_eval_each(n: i32) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_each_{n}"),
&format!("(1..{n}) | each {{|_| 1 }} | ignore"),
stack,
engine,
)
}
fn bench_eval_par_each(n: i32) -> impl IntoBenchmarks {
let engine = setup_engine();
let stack = Stack::new();
bench_command(
&format!("eval_par_each_{n}"),
&format!("(1..{}) | par-each -t 2 {{|_| 1 }} | ignore", n),
stack,
engine,
)
}
fn bench_eval_default_config() -> impl IntoBenchmarks {
let default_env = get_default_config().as_bytes().to_vec();
let fname = "default_config.nu".to_string();
bench_eval_source(
"eval_default_config",
fname,
default_env,
Stack::new(),
setup_engine(),
)
}
fn bench_eval_default_env() -> impl IntoBenchmarks {
let default_env = get_default_env().as_bytes().to_vec();
let fname = "default_env.nu".to_string();
bench_eval_source(
"eval_default_env",
fname,
default_env,
Stack::new(),
setup_engine(),
)
}
fn encode_json(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = Rc::new(PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
));
let encoder = Rc::new(EncodingType::try_from_bytes(b"json").unwrap());
[benchmark_fn(
format!("encode_json_{}_{}", row_cnt, col_cnt),
move |b| {
let encoder = encoder.clone();
let test_data = test_data.clone();
b.iter(move || {
let mut res = Vec::new();
encoder.encode(&*test_data, &mut res).unwrap();
})
},
)]
}
fn encode_msgpack(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = Rc::new(PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
));
let encoder = Rc::new(EncodingType::try_from_bytes(b"msgpack").unwrap());
[benchmark_fn(
format!("encode_msgpack_{}_{}", row_cnt, col_cnt),
move |b| {
let encoder = encoder.clone();
let test_data = test_data.clone();
b.iter(move || {
let mut res = Vec::new();
encoder.encode(&*test_data, &mut res).unwrap();
})
},
)]
}
fn decode_json(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
);
let encoder = EncodingType::try_from_bytes(b"json").unwrap();
let mut res = vec![];
encoder.encode(&test_data, &mut res).unwrap();
[benchmark_fn(
format!("decode_json_{}_{}", row_cnt, col_cnt),
move |b| {
let res = res.clone();
b.iter(move || {
let mut binary_data = std::io::Cursor::new(res.clone());
binary_data.set_position(0);
let _: Result<Option<PluginOutput>, _> =
black_box(encoder.decode(&mut binary_data));
})
},
)]
}
fn decode_msgpack(row_cnt: usize, col_cnt: usize) -> impl IntoBenchmarks {
let test_data = PluginOutput::CallResponse(
0,
PluginCallResponse::value(encoding_test_data(row_cnt, col_cnt)),
);
let encoder = EncodingType::try_from_bytes(b"msgpack").unwrap();
let mut res = vec![];
encoder.encode(&test_data, &mut res).unwrap();
[benchmark_fn(
format!("decode_msgpack_{}_{}", row_cnt, col_cnt),
move |b| {
let res = res.clone();
b.iter(move || {
let mut binary_data = std::io::Cursor::new(res.clone());
binary_data.set_position(0);
let _: Result<Option<PluginOutput>, _> =
black_box(encoder.decode(&mut binary_data));
})
},
)]
}
tango_benchmarks!(
bench_load_standard_lib(),
// Data types
// Record
bench_record_create(1),
bench_record_create(10),
bench_record_create(100),
bench_record_create(1_000),
bench_record_flat_access(1),
bench_record_flat_access(10),
bench_record_flat_access(100),
bench_record_flat_access(1_000),
bench_record_nested_access(1),
bench_record_nested_access(2),
bench_record_nested_access(4),
bench_record_nested_access(8),
bench_record_nested_access(16),
bench_record_nested_access(32),
bench_record_nested_access(64),
bench_record_nested_access(128),
// Table
bench_table_create(1),
bench_table_create(10),
bench_table_create(100),
bench_table_create(1_000),
bench_table_get(1),
bench_table_get(10),
bench_table_get(100),
bench_table_get(1_000),
bench_table_select(1),
bench_table_select(10),
bench_table_select(100),
bench_table_select(1_000),
// Eval
// Interleave
bench_eval_interleave(100),
bench_eval_interleave(1_000),
bench_eval_interleave(10_000),
bench_eval_interleave_with_ctrlc(100),
bench_eval_interleave_with_ctrlc(1_000),
bench_eval_interleave_with_ctrlc(10_000),
// For
bench_eval_for(1),
bench_eval_for(10),
bench_eval_for(100),
bench_eval_for(1_000),
bench_eval_for(10_000),
// Each
bench_eval_each(1),
bench_eval_each(10),
bench_eval_each(100),
bench_eval_each(1_000),
bench_eval_each(10_000),
// Par-Each
bench_eval_par_each(1),
bench_eval_par_each(10),
bench_eval_par_each(100),
bench_eval_par_each(1_000),
bench_eval_par_each(10_000),
// Config
bench_eval_default_config(),
// Env
bench_eval_default_env(),
// Encode
// Json
encode_json(100, 5),
encode_json(10000, 15),
// MsgPack
encode_msgpack(100, 5),
encode_msgpack(10000, 15),
// Decode
// Json
decode_json(100, 5),
decode_json(10000, 15),
// MsgPack
decode_msgpack(100, 5),
decode_msgpack(10000, 15)
); );
criterion_main!(benches);
tango_main!();

View File

@ -1,12 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
DIR=$(readlink -f $(dirname "${BASH_SOURCE[0]}"))
REPO_ROOT=$(dirname $DIR)
echo "---------------------------------------------------------------" echo "---------------------------------------------------------------"
echo "Building nushell (nu) and all the plugins" echo "Building nushell (nu) with dataframes and all the plugins"
echo "---------------------------------------------------------------" echo "---------------------------------------------------------------"
echo "" echo ""
@ -19,17 +15,11 @@ NU_PLUGINS=(
) )
echo "Building nushell" echo "Building nushell"
( cargo build --features=dataframe
cd $REPO_ROOT
cargo build --locked
)
for plugin in "${NU_PLUGINS[@]}" for plugin in "${NU_PLUGINS[@]}"
do do
echo '' && cd crates/"$plugin"
echo "Building $plugin..." echo "Building $plugin..."
echo "-----------------------------" echo "-----------------------------"
( cargo build && cd ../..
cd "$REPO_ROOT/crates/$plugin"
cargo build
)
done done

36
build-all-windows.cmd Normal file
View File

@ -0,0 +1,36 @@
@echo off
@echo -------------------------------------------------------------------
@echo Building nushell (nu.exe) with dataframes and all the plugins
@echo -------------------------------------------------------------------
@echo.
echo Building nushell.exe
cargo build --features=dataframe
@echo.
@cd crates\nu_plugin_example
echo Building nu_plugin_example.exe
cargo build
@echo.
@cd ..\..\crates\nu_plugin_gstat
echo Building nu_plugin_gstat.exe
cargo build
@echo.
@cd ..\..\crates\nu_plugin_inc
echo Building nu_plugin_inc.exe
cargo build
@echo.
@cd ..\..\crates\nu_plugin_query
echo Building nu_plugin_query.exe
cargo build
@echo.
@cd ..\..\crates\nu_plugin_custom_values
echo Building nu_plugin_custom_values.exe
cargo build
@echo.
@cd ..\..

24
build-all.nu Normal file
View File

@ -0,0 +1,24 @@
echo '-------------------------------------------------------------------'
echo 'Building nushell (nu) with dataframes and all the plugins'
echo '-------------------------------------------------------------------'
echo $'(char nl)Building nushell'
echo '----------------------------'
cargo build --features=dataframe
let plugins = [
nu_plugin_inc,
nu_plugin_gstat,
nu_plugin_query,
nu_plugin_example,
nu_plugin_custom_values,
nu_plugin_formats,
]
for plugin in $plugins {
$'(char nl)Building ($plugin)'
'----------------------------'
cd $'crates/($plugin)'
cargo build
ignore
}

13
build.rs Normal file
View File

@ -0,0 +1,13 @@
#[cfg(windows)]
fn main() {
let mut res = winres::WindowsResource::new();
res.set("ProductName", "Nushell");
res.set("FileDescription", "Nushell");
res.set("LegalCopyright", "Copyright (C) 2022");
res.set_icon("assets/nu_logo.ico");
res.compile()
.expect("Failed to run the Windows resource compiler (rc.exe)");
}
#[cfg(not(windows))]
fn main() {}

17
codecov.yml Normal file
View File

@ -0,0 +1,17 @@
coverage:
status:
project:
default:
target: 55%
threshold: 2%
patch:
default:
informational: true
comment:
layout: reach, diff, files
behavior: default
require_base: yes
require_head: yes
after_n_builds: 1 # Disabled windows else: 2

54
coverage-local.nu Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env nu
let start = (date now)
# Script to generate coverage locally
#
# Output: `lcov.info` file
#
# Relies on `cargo-llvm-cov`. Install via `cargo install cargo-llvm-cov`
# https://github.com/taiki-e/cargo-llvm-cov
# You probably have to run `cargo llvm-cov clean` once manually,
# as you have to confirm to install additional tooling for your rustup toolchain.
# Else the script might stall waiting for your `y<ENTER>`
# Some of the internal tests rely on the exact cargo profile
# (This is somewhat criminal itself)
# but we have to signal to the tests that we use the `ci` `--profile`
let-env NUSHELL_CARGO_TARGET = "ci"
# Manual gathering of coverage to catch invocation of the `nu` binary.
# This is relevant for tests using the `nu!` macro from `nu-test-support`
# see: https://github.com/taiki-e/cargo-llvm-cov#get-coverage-of-external-tests
print "Setting up environment variables for coverage"
# Enable LLVM coverage tracking through environment variables
# show env outputs .ini/.toml style description of the variables
# In order to use from toml, we need to make sure our string literals are single quoted
# This is especially important when running on Windows since "C:\blah" is treated as an escape
cargo llvm-cov show-env | str replace (char dq) (char sq) -a | from toml | load-env
print "Cleaning up coverage data"
cargo llvm-cov clean --workspace
print "Building with workspace and profile=ci"
# Apparently we need to explicitly build the necessary parts
# using the `--profile=ci` is basically `debug` build with unnecessary symbols stripped
# leads to smaller binaries and potential savings when compiling and running
cargo build --workspace --profile=ci
print "Running tests with --workspace and profile=ci"
cargo test --workspace --profile=ci
# You need to provide the used profile to find the raw data
print "Generating coverage report as lcov.info"
cargo llvm-cov report --lcov --output-path lcov.info --profile=ci
let end = (date now)
$"Coverage generation took ($end - $start)."
# To display the coverage in your editor see:
#
# - https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters
# - https://github.com/umaumax/vim-lcov
# - https://github.com/andythigpen/nvim-coverage (probably needs some additional config)

View File

@ -1,8 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DIR=$(readlink -f $(dirname "${BASH_SOURCE[0]}"))
REPO_ROOT=$(dirname $DIR)
# Script to generate coverage locally # Script to generate coverage locally
# #
# Output: `lcov.info` file # Output: `lcov.info` file
@ -17,25 +14,22 @@ REPO_ROOT=$(dirname $DIR)
# Some of the internal tests rely on the exact cargo profile # Some of the internal tests rely on the exact cargo profile
# (This is somewhat criminal itself) # (This is somewhat criminal itself)
# but we have to signal to the tests that we use the `ci` `--profile` # but we have to signal to the tests that we use the `ci` `--profile`
export NUSHELL_CARGO_PROFILE=ci export NUSHELL_CARGO_TARGET=ci
# Manual gathering of coverage to catch invocation of the `nu` binary. # Manual gathering of coverage to catch invocation of the `nu` binary.
# This is relevant for tests using the `nu!` macro from `nu-test-support` # This is relevant for tests using the `nu!` macro from `nu-test-support`
# see: https://github.com/taiki-e/cargo-llvm-cov#get-coverage-of-external-tests # see: https://github.com/taiki-e/cargo-llvm-cov#get-coverage-of-external-tests
( # Enable LLVM coverage tracking through environment variables
cd $REPO_ROOT source <(cargo llvm-cov show-env --export-prefix)
# Enable LLVM coverage tracking through environment variables cargo llvm-cov clean --workspace
source <(cargo llvm-cov show-env --export-prefix) # Apparently we need to explicitly build the necessary parts
cargo llvm-cov clean --workspace # using the `--profile=ci` is basically `debug` build with unnecessary symbols stripped
# Apparently we need to explicitly build the necessary parts # leads to smaller binaries and potential savings when compiling and running
# using the `--profile=ci` is basically `debug` build with unnecessary symbols stripped cargo build --workspace --profile=ci
# leads to smaller binaries and potential savings when compiling and running cargo test --workspace --profile=ci
cargo build --workspace --profile=ci # You need to provide the used profile to find the raw data
cargo test --workspace --profile=ci cargo llvm-cov report --lcov --output-path lcov.info --profile=ci
# You need to provide the used profile to find the raw data
cargo llvm-cov report --lcov --output-path lcov.info --profile=ci
)
# To display the coverage in your editor see: # To display the coverage in your editor see:
# #

View File

@ -5,45 +5,39 @@ repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
name = "nu-cli" name = "nu-cli"
version = "0.94.0" version = "0.77.1"
[lib] [lib]
bench = false bench = false
[dev-dependencies] [dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.0" } nu-test-support = { path = "../nu-test-support", version = "0.77.1" }
nu-command = { path = "../nu-command", version = "0.94.0" } nu-command = { path = "../nu-command", version = "0.77.1" }
nu-test-support = { path = "../nu-test-support", version = "0.94.0" } rstest = { version = "0.16.0", default-features = false }
rstest = { workspace = true, default-features = false }
tempfile = { workspace = true }
[dependencies] [dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.0" } nu-engine = { path = "../nu-engine", version = "0.77.1" }
nu-engine = { path = "../nu-engine", version = "0.94.0" } nu-path = { path = "../nu-path", version = "0.77.1" }
nu-path = { path = "../nu-path", version = "0.94.0" } nu-parser = { path = "../nu-parser", version = "0.77.1" }
nu-parser = { path = "../nu-parser", version = "0.94.0" } nu-protocol = { path = "../nu-protocol", version = "0.77.1" }
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.94.0", optional = true } nu-utils = { path = "../nu-utils", version = "0.77.1" }
nu-protocol = { path = "../nu-protocol", version = "0.94.0" } nu-color-config = { path = "../nu-color-config", version = "0.77.1" }
nu-utils = { path = "../nu-utils", version = "0.94.0" }
nu-color-config = { path = "../nu-color-config", version = "0.94.0" }
nu-ansi-term = { workspace = true }
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
chrono = { default-features = false, features = ["std"], workspace = true } nu-ansi-term = "0.47.0"
crossterm = { workspace = true } reedline = { version = "0.17.0", features = ["bashisms", "sqlite"] }
fancy-regex = { workspace = true }
fuzzy-matcher = { workspace = true } atty = "0.2.14"
is_executable = { workspace = true } chrono = { default-features = false, features = ["std"], version = "0.4.23" }
log = { workspace = true } crossterm = "0.24.0"
miette = { workspace = true, features = ["fancy-no-backtrace"] } fancy-regex = "0.11.0"
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] } fuzzy-matcher = "0.3.7"
once_cell = { workspace = true } is_executable = "1.0.1"
percent-encoding = { workspace = true } once_cell = "1.17.0"
sysinfo = { workspace = true } log = "0.4"
unicode-segmentation = { workspace = true } miette = { version = "5.5.0", features = ["fancy-no-backtrace"] }
uuid = { workspace = true, features = ["v4"] } percent-encoding = "2"
which = { workspace = true } sysinfo = "0.28.2"
thiserror = "1.0.31"
[features] [features]
plugin = ["nu-plugin-engine"] plugin = []
system-clipboard = ["reedline/system_clipboard"]

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers Copyright (c) 2019 - 2022 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -0,0 +1,73 @@
use crate::util::report_error;
use log::info;
use miette::Result;
use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse;
use nu_protocol::engine::Stack;
use nu_protocol::{
engine::{EngineState, StateWorkingSet},
PipelineData, Spanned, Value,
};
/// Run a command (or commands) given to us by the user
pub fn evaluate_commands(
commands: &Spanned<String>,
engine_state: &mut EngineState,
stack: &mut Stack,
input: PipelineData,
table_mode: Option<Value>,
) -> Result<Option<i64>> {
// Translate environment variables from Strings to Values
if let Some(e) = convert_env_values(engine_state, stack) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
std::process::exit(1);
}
// Parse the source code
let (block, delta) = {
if let Some(ref t_mode) = table_mode {
let mut config = engine_state.get_config().clone();
config.table_mode = t_mode.as_string()?;
engine_state.set_config(&config);
}
let mut working_set = StateWorkingSet::new(engine_state);
let (output, err) = parse(&mut working_set, None, commands.item.as_bytes(), false, &[]);
if let Some(err) = err {
report_error(&working_set, &err);
std::process::exit(1);
}
(output, working_set.render())
};
// Update permanent state
if let Err(err) = engine_state.merge_delta(delta) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}
// Run the block
let exit_code = match eval_block(engine_state, stack, &block, input, false, false) {
Ok(pipeline_data) => {
let mut config = engine_state.get_config().clone();
if let Some(t_mode) = table_mode {
config.table_mode = t_mode.as_string()?;
}
crate::eval_file::print_table_or_error(engine_state, stack, pipeline_data, &mut config)
}
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
std::process::exit(1);
}
};
info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(exit_code)
}

View File

@ -1,35 +0,0 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct Commandline;
impl Command for Commandline {
fn name(&self) -> &str {
"commandline"
}
fn signature(&self) -> Signature {
Signature::build("commandline")
.input_output_types(vec![(Type::Nothing, Type::String)])
.category(Category::Core)
}
fn usage(&self) -> &str {
"View the current command line input buffer."
}
fn search_terms(&self) -> Vec<&str> {
vec!["repl", "interactive"]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let repl = engine_state.repl_state.lock().expect("repl state mutex");
Ok(Value::string(repl.buffer.clone(), call.head).into_pipeline_data())
}
}

View File

@ -1,66 +0,0 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"commandline edit"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch(
"append",
"appends the string to the end of the buffer",
Some('a'),
)
.switch(
"insert",
"inserts the string into the buffer at the cursor position",
Some('i'),
)
.switch(
"replace",
"replaces the current contents of the buffer (default)",
Some('r'),
)
.required(
"str",
SyntaxShape::String,
"the string to perform the operation with",
)
.category(Category::Core)
}
fn usage(&self) -> &str {
"Modify the current command line input buffer."
}
fn search_terms(&self) -> Vec<&str> {
vec!["repl", "interactive"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let str: String = call.req(engine_state, stack, 0)?;
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
if call.has_flag(engine_state, stack, "append")? {
repl.buffer.push_str(&str);
} else if call.has_flag(engine_state, stack, "insert")? {
let cursor_pos = repl.cursor_pos;
repl.buffer.insert_str(cursor_pos, &str);
repl.cursor_pos += str.len();
} else {
repl.buffer = str;
repl.cursor_pos = repl.buffer.len();
}
Ok(Value::nothing(call.head).into_pipeline_data())
}
}

View File

@ -1,52 +0,0 @@
use nu_engine::command_prelude::*;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"commandline get-cursor"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::Int)])
.allow_variants_without_examples(true)
.category(Category::Core)
}
fn usage(&self) -> &str {
"Get the current cursor position."
}
fn search_terms(&self) -> Vec<&str> {
vec!["repl", "interactive"]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let repl = engine_state.repl_state.lock().expect("repl state mutex");
let char_pos = repl
.buffer
.grapheme_indices(true)
.chain(std::iter::once((repl.buffer.len(), "")))
.position(|(i, _c)| i == repl.cursor_pos)
.expect("Cursor position isn't on a grapheme boundary");
match i64::try_from(char_pos) {
Ok(pos) => Ok(Value::int(pos, call.head).into_pipeline_data()),
Err(e) => Err(ShellError::GenericError {
error: "Failed to convert cursor position to int".to_string(),
msg: e.to_string(),
span: None,
help: None,
inner: vec![],
}),
}
}
}

View File

@ -1,9 +0,0 @@
mod commandline_;
mod edit;
mod get_cursor;
mod set_cursor;
pub use commandline_::Commandline;
pub use edit::SubCommand as CommandlineEdit;
pub use get_cursor::SubCommand as CommandlineGetCursor;
pub use set_cursor::SubCommand as CommandlineSetCursor;

View File

@ -1,65 +0,0 @@
use nu_engine::command_prelude::*;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"commandline set-cursor"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch(
"end",
"set the current cursor position to the end of the buffer",
Some('e'),
)
.optional("pos", SyntaxShape::Int, "Cursor position to be set")
.category(Category::Core)
}
fn usage(&self) -> &str {
"Set the current cursor position."
}
fn search_terms(&self) -> Vec<&str> {
vec!["repl", "interactive"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
if let Some(pos) = call.opt::<i64>(engine_state, stack, 0)? {
repl.cursor_pos = if pos <= 0 {
0usize
} else {
repl.buffer
.grapheme_indices(true)
.map(|(i, _c)| i)
.nth(pos as usize)
.unwrap_or(repl.buffer.len())
};
Ok(Value::nothing(call.head).into_pipeline_data())
} else if call.has_flag(engine_state, stack, "end")? {
repl.cursor_pos = repl.buffer.len();
Ok(Value::nothing(call.head).into_pipeline_data())
} else {
Err(ShellError::GenericError {
error: "Required a positional argument or a flag".to_string(),
msg: "".to_string(),
span: None,
help: None,
inner: vec![],
})
}
}
}

View File

@ -1,35 +0,0 @@
use crate::commands::*;
use nu_protocol::engine::{EngineState, StateWorkingSet};
pub fn add_cli_context(mut engine_state: EngineState) -> EngineState {
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
macro_rules! bind_command {
( $( $command:expr ),* $(,)? ) => {
$( working_set.add_decl(Box::new($command)); )*
};
}
bind_command! {
Commandline,
CommandlineEdit,
CommandlineGetCursor,
CommandlineSetCursor,
History,
HistorySession,
Keybindings,
KeybindingsDefault,
KeybindingsList,
KeybindingsListen,
};
working_set.render()
};
if let Err(err) = engine_state.merge_delta(delta) {
eprintln!("Error creating CLI command context: {err:?}");
}
engine_state
}

View File

@ -1,5 +0,0 @@
mod history_;
mod history_session;
pub use history_::History;
pub use history_session::HistorySession;

View File

@ -1,54 +0,0 @@
use nu_engine::command_prelude::*;
use reedline::get_reedline_default_keybindings;
#[derive(Clone)]
pub struct KeybindingsDefault;
impl Command for KeybindingsDefault {
fn name(&self) -> &str {
"keybindings default"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::table())])
}
fn usage(&self) -> &str {
"List default keybindings."
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Get list with default keybindings",
example: "keybindings default",
result: None,
}]
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let records = get_reedline_default_keybindings()
.into_iter()
.map(|(mode, modifier, code, event)| {
Value::record(
record! {
"mode" => Value::string(mode, call.head),
"modifier" => Value::string(modifier, call.head),
"code" => Value::string(code, call.head),
"event" => Value::string(event, call.head),
},
call.head,
)
})
.collect();
Ok(Value::list(records, call.head).into_pipeline_data())
}
}

View File

@ -1,173 +0,0 @@
use crossterm::{
event::Event, event::KeyCode, event::KeyEvent, execute, terminal, QueueableCommand,
};
use nu_engine::command_prelude::*;
use std::io::{stdout, Write};
#[derive(Clone)]
pub struct KeybindingsListen;
impl Command for KeybindingsListen {
fn name(&self) -> &str {
"keybindings listen"
}
fn usage(&self) -> &str {
"Get input from the user."
}
fn extra_usage(&self) -> &str {
"This is an internal debugging tool. For better output, try `input listen --types [key]`"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
println!("Type any key combination to see key details. Press ESC to abort.");
match print_events(engine_state) {
Ok(v) => Ok(v.into_pipeline_data()),
Err(e) => {
terminal::disable_raw_mode()?;
Err(ShellError::GenericError {
error: "Error with input".into(),
msg: "".into(),
span: None,
help: Some(e.to_string()),
inner: vec![],
})
}
}
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Type and see key event codes",
example: "keybindings listen",
result: None,
}]
}
}
pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
let config = engine_state.get_config();
stdout().flush()?;
terminal::enable_raw_mode()?;
if config.use_kitty_protocol {
if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() {
println!("WARN: The terminal doesn't support use_kitty_protocol config.\r");
}
// enable kitty protocol
//
// Note that, currently, only the following support this protocol:
// * [kitty terminal](https://sw.kovidgoyal.net/kitty/)
// * [foot terminal](https://codeberg.org/dnkl/foot/issues/319)
// * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html)
// * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131)
// * [neovim text editor](https://github.com/neovim/neovim/pull/18181)
// * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103)
// * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138)
//
// Refer to https://sw.kovidgoyal.net/kitty/keyboard-protocol/ if you're curious.
let _ = execute!(
stdout(),
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)
);
}
let mut stdout = std::io::BufWriter::new(std::io::stderr());
loop {
let event = crossterm::event::read()?;
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
// stdout.queue(crossterm::style::Print(format!("event: {:?}", &event)))?;
// stdout.queue(crossterm::style::Print("\r\n"))?;
// Get a record
let v = print_events_helper(event)?;
// Print out the record
let o = match v {
Value::Record { val, .. } => val
.iter()
.map(|(x, y)| format!("{}: {}", x, y.to_expanded_string("", config)))
.collect::<Vec<String>>()
.join(", "),
_ => "".to_string(),
};
stdout.queue(crossterm::style::Print(o))?;
stdout.queue(crossterm::style::Print("\r\n"))?;
stdout.flush()?;
}
if config.use_kitty_protocol {
let _ = execute!(
std::io::stdout(),
crossterm::event::PopKeyboardEnhancementFlags
);
}
terminal::disable_raw_mode()?;
Ok(Value::nothing(Span::unknown()))
}
// this fn is totally ripped off from crossterm's examples
// it's really a diagnostic routine to see if crossterm is
// even seeing the events. if you press a key and no events
// are printed, it's a good chance your terminal is eating
// those events.
fn print_events_helper(event: Event) -> Result<Value, ShellError> {
if let Event::Key(KeyEvent {
code,
modifiers,
kind,
state,
}) = event
{
match code {
KeyCode::Char(c) => {
let record = record! {
"char" => Value::string(format!("{c}"), Span::unknown()),
"code" => Value::string(format!("{:#08x}", u32::from(c)), Span::unknown()),
"modifier" => Value::string(format!("{modifiers:?}"), Span::unknown()),
"flags" => Value::string(format!("{modifiers:#08b}"), Span::unknown()),
"kind" => Value::string(format!("{kind:?}"), Span::unknown()),
"state" => Value::string(format!("{state:?}"), Span::unknown()),
};
Ok(Value::record(record, Span::unknown()))
}
_ => {
let record = record! {
"code" => Value::string(format!("{code:?}"), Span::unknown()),
"modifier" => Value::string(format!("{modifiers:?}"), Span::unknown()),
"flags" => Value::string(format!("{modifiers:#08b}"), Span::unknown()),
"kind" => Value::string(format!("{kind:?}"), Span::unknown()),
"state" => Value::string(format!("{state:?}"), Span::unknown()),
};
Ok(Value::record(record, Span::unknown()))
}
}
} else {
let record = record! { "event" => Value::string(format!("{event:?}"), Span::unknown()) };
Ok(Value::record(record, Span::unknown()))
}
}

View File

@ -1,30 +1,25 @@
use crate::completions::{CompletionOptions, SortBy}; use crate::completions::{CompletionOptions, SortBy};
use nu_protocol::{ use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
engine::{Stack, StateWorkingSet},
levenshtein_distance, Span,
};
use reedline::Suggestion; use reedline::Suggestion;
// Completer trait represents the three stages of the completion // Completer trait represents the three stages of the completion
// fetch, filter and sort // fetch, filter and sort
pub trait Completer { pub trait Completer {
#[allow(clippy::too_many_arguments)]
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion>; ) -> Vec<Suggestion>;
fn get_sort_by(&self) -> SortBy { fn get_sort_by(&self) -> SortBy {
SortBy::Ascending SortBy::Ascending
} }
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> { fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string(); let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut filtered_items = items; let mut filtered_items = items;
@ -32,13 +27,13 @@ pub trait Completer {
match self.get_sort_by() { match self.get_sort_by() {
SortBy::LevenshteinDistance => { SortBy::LevenshteinDistance => {
filtered_items.sort_by(|a, b| { filtered_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); let b_distance = levenshtein_distance(&prefix_str, &b.value);
a_distance.cmp(&b_distance) a_distance.cmp(&b_distance)
}); });
} }
SortBy::Ascending => { SortBy::Ascending => {
filtered_items.sort_by(|a, b| a.suggestion.value.cmp(&b.suggestion.value)); filtered_items.sort_by(|a, b| a.value.cmp(&b.value));
} }
SortBy::None => {} SortBy::None => {}
}; };
@ -46,25 +41,3 @@ pub trait Completer {
filtered_items filtered_items
} }
} }
#[derive(Debug, Default, PartialEq)]
pub struct SemanticSuggestion {
pub suggestion: Suggestion,
pub kind: Option<SuggestionKind>,
}
// TODO: think about name: maybe suggestion context?
#[derive(Clone, Debug, PartialEq)]
pub enum SuggestionKind {
Command(nu_protocol::engine::CommandType),
Type(nu_protocol::Type),
}
impl From<Suggestion> for SemanticSuggestion {
fn from(suggestion: Suggestion) -> Self {
Self {
suggestion,
..Default::default()
}
}
}

View File

@ -1,17 +1,14 @@
use crate::{ use crate::completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy};
completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy},
SuggestionKind,
};
use nu_parser::FlatShape; use nu_parser::FlatShape;
use nu_protocol::{ use nu_protocol::{
engine::{CachedFile, Stack, StateWorkingSet}, engine::{EngineState, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::sync::Arc;
use super::SemanticSuggestion;
pub struct CommandCompletion { pub struct CommandCompletion {
engine_state: Arc<EngineState>,
flattened: Vec<(Span, FlatShape)>, flattened: Vec<(Span, FlatShape)>,
flat_shape: FlatShape, flat_shape: FlatShape,
force_completion_after_space: bool, force_completion_after_space: bool,
@ -19,11 +16,14 @@ pub struct CommandCompletion {
impl CommandCompletion { impl CommandCompletion {
pub fn new( pub fn new(
engine_state: Arc<EngineState>,
_: &StateWorkingSet,
flattened: Vec<(Span, FlatShape)>, flattened: Vec<(Span, FlatShape)>,
flat_shape: FlatShape, flat_shape: FlatShape,
force_completion_after_space: bool, force_completion_after_space: bool,
) -> Self { ) -> Self {
Self { Self {
engine_state,
flattened, flattened,
flat_shape, flat_shape,
force_completion_after_space, force_completion_after_space,
@ -32,26 +32,22 @@ impl CommandCompletion {
fn external_command_completion( fn external_command_completion(
&self, &self,
working_set: &StateWorkingSet,
prefix: &str, prefix: &str,
match_algorithm: MatchAlgorithm, match_algorithm: MatchAlgorithm,
) -> Vec<String> { ) -> Vec<String> {
let mut executables = vec![]; let mut executables = vec![];
// os agnostic way to get the PATH env var // os agnostic way to get the PATH env var
let paths = working_set.permanent_state.get_path_env_var(); let paths = self.engine_state.get_path_env_var();
if let Some(paths) = paths { if let Some(paths) = paths {
if let Ok(paths) = paths.as_list() { if let Ok(paths) = paths.as_list() {
for path in paths { for path in paths {
let path = path.coerce_str().unwrap_or_default(); let path = path.as_string().unwrap_or_default();
if let Ok(mut contents) = std::fs::read_dir(path.as_ref()) { if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() { while let Some(Ok(item)) = contents.next() {
if working_set if self.engine_state.config.max_external_completion_results
.permanent_state
.config
.max_external_completion_results
> executables.len() as i64 > executables.len() as i64
&& !executables.contains( && !executables.contains(
&item &item
@ -87,62 +83,61 @@ impl CommandCompletion {
offset: usize, offset: usize,
find_externals: bool, find_externals: bool,
match_algorithm: MatchAlgorithm, match_algorithm: MatchAlgorithm,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let partial = working_set.get_span_contents(span); let partial = working_set.get_span_contents(span);
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial); let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
let mut results = working_set let results = working_set
.find_commands_by_predicate(filter_predicate, true) .find_commands_by_predicate(filter_predicate)
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| Suggestion {
suggestion: Suggestion { value: String::from_utf8_lossy(&x.0).to_string(),
value: String::from_utf8_lossy(&x.0).to_string(), description: x.1,
description: x.1, extra: None,
style: None, span: reedline::Span::new(span.start - offset, span.end - offset),
extra: None, append_whitespace: true,
span: reedline::Span::new(span.start - offset, span.end - offset), });
append_whitespace: true,
}, let results_aliases = working_set
kind: Some(SuggestionKind::Command(x.2)), .find_aliases_by_predicate(filter_predicate)
}) .into_iter()
.collect::<Vec<_>>(); .map(move |x| Suggestion {
value: String::from_utf8_lossy(&x).to_string(),
description: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
});
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
let partial = working_set.get_span_contents(span); let partial = working_set.get_span_contents(span);
let partial = String::from_utf8_lossy(partial).to_string(); let partial = String::from_utf8_lossy(partial).to_string();
if find_externals { if find_externals {
let results_external = self let results_external = self
.external_command_completion(working_set, &partial, match_algorithm) .external_command_completion(&partial, match_algorithm)
.into_iter() .into_iter()
.map(move |x| SemanticSuggestion { .map(move |x| Suggestion {
suggestion: Suggestion { value: x,
value: x, description: None,
description: None, extra: None,
style: None, span: reedline::Span::new(span.start - offset, span.end - offset),
extra: None, append_whitespace: true,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
},
// TODO: is there a way to create a test?
kind: None,
}); });
let results_strings: Vec<String> = let results_strings: Vec<String> =
results.iter().map(|x| x.suggestion.value.clone()).collect(); results.clone().into_iter().map(|x| x.value).collect();
for external in results_external { for external in results_external {
if results_strings.contains(&external.suggestion.value) { if results_strings.contains(&external.value) {
results.push(SemanticSuggestion { results.push(Suggestion {
suggestion: Suggestion { value: format!("^{}", external.value),
value: format!("^{}", external.suggestion.value), description: None,
description: None, extra: None,
style: None, span: external.span,
extra: None, append_whitespace: true,
span: external.suggestion.span,
append_whitespace: true,
},
kind: external.kind,
}) })
} else { } else {
results.push(external) results.push(external)
@ -160,13 +155,12 @@ impl Completer for CommandCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack,
_prefix: Vec<u8>, _prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let last = self let last = self
.flattened .flattened
.iter() .iter()
@ -175,7 +169,7 @@ impl Completer for CommandCompletion {
.take_while(|x| { .take_while(|x| {
matches!( matches!(
x.1, x.1,
FlatShape::InternalCall(_) FlatShape::InternalCall
| FlatShape::External | FlatShape::External
| FlatShape::ExternalArg | FlatShape::ExternalArg
| FlatShape::Literal | FlatShape::Literal
@ -203,7 +197,7 @@ impl Completer for CommandCompletion {
let config = working_set.get_config(); let config = working_set.get_config();
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External) let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall(_)) || matches!(self.flat_shape, nu_parser::FlatShape::InternalCall)
|| ((span.end - span.start) == 0) || ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents()) || is_passthrough_command(working_set.delta.get_file_contents())
{ {
@ -223,7 +217,10 @@ impl Completer for CommandCompletion {
vec![] vec![]
}; };
subcommands.into_iter().chain(commands).collect::<Vec<_>>() subcommands
.into_iter()
.chain(commands.into_iter())
.collect::<Vec<_>>()
} }
fn get_sort_by(&self) -> SortBy { fn get_sort_by(&self) -> SortBy {
@ -244,16 +241,15 @@ pub fn find_non_whitespace_index(contents: &[u8], start: usize) -> usize {
} }
} }
pub fn is_passthrough_command(working_set_file_contents: &[CachedFile]) -> bool { pub fn is_passthrough_command(working_set_file_contents: &[(Vec<u8>, usize, usize)]) -> bool {
for cached_file in working_set_file_contents { for (contents, _, _) in working_set_file_contents {
let contents = &cached_file.content;
let last_pipe_pos_rev = contents.iter().rev().position(|x| x == &b'|'); let last_pipe_pos_rev = contents.iter().rev().position(|x| x == &b'|');
let last_pipe_pos = last_pipe_pos_rev.map(|x| contents.len() - x).unwrap_or(0); let last_pipe_pos = last_pipe_pos_rev.map(|x| contents.len() - x).unwrap_or(0);
let cur_pos = find_non_whitespace_index(contents, last_pipe_pos); let cur_pos = find_non_whitespace_index(contents, last_pipe_pos);
let result = match contents.get(cur_pos..) { let result = match contents.get(cur_pos..) {
Some(contents) => contents.starts_with(b"sudo ") || contents.starts_with(b"doas "), Some(contents) => contents.starts_with(b"sudo "),
None => false, None => false,
}; };
if result { if result {
@ -266,12 +262,10 @@ pub fn is_passthrough_command(working_set_file_contents: &[CachedFile]) -> bool
#[cfg(test)] #[cfg(test)]
mod command_completions_tests { mod command_completions_tests {
use super::*; use super::*;
use nu_protocol::engine::EngineState;
use std::sync::Arc;
#[test] #[test]
fn test_find_non_whitespace_index() { fn test_find_non_whitespace_index() {
let commands = [ let commands = vec![
(" hello", 4), (" hello", 4),
("sudo ", 0), ("sudo ", 0),
(" sudo ", 2), (" sudo ", 2),
@ -284,14 +278,14 @@ mod command_completions_tests {
(" hello sud", 1), (" hello sud", 1),
]; ];
for (idx, ele) in commands.iter().enumerate() { for (idx, ele) in commands.iter().enumerate() {
let index = find_non_whitespace_index(ele.0.as_bytes(), 0); let index = find_non_whitespace_index(&Vec::from(ele.0.as_bytes()), 0);
assert_eq!(index, ele.1, "Failed on index {}", idx); assert_eq!(index, ele.1, "Failed on index {}", idx);
} }
} }
#[test] #[test]
fn test_is_last_command_passthrough() { fn test_is_last_command_passthrough() {
let commands = [ let commands = vec![
(" hello", false), (" hello", false),
(" sudo ", true), (" sudo ", true),
("sudo ", true), ("sudo ", true),
@ -313,11 +307,11 @@ mod command_completions_tests {
let input = ele.0.as_bytes(); let input = ele.0.as_bytes();
let mut engine_state = EngineState::new(); let mut engine_state = EngineState::new();
engine_state.add_file("test.nu".into(), Arc::new([])); engine_state.add_file("test.nu".into(), vec![]);
let delta = { let delta = {
let mut working_set = StateWorkingSet::new(&engine_state); let mut working_set = StateWorkingSet::new(&engine_state);
let _ = working_set.add_file("child.nu".into(), input); working_set.add_file("child.nu".into(), input);
working_set.render() working_set.render()
}; };

View File

@ -1,19 +1,17 @@
use crate::completions::{ use crate::completions::{
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion, CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
DotNuCompletion, FileCompletion, FlagCompletion, VariableCompletion, DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion,
}; };
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::{flatten_pipeline_element, parse, FlatShape}; use nu_parser::{flatten_expression, parse, FlatShape};
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug, ast::PipelineElement,
engine::{Closure, EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
PipelineData, Span, Value, BlockId, PipelineData, Span, Value,
}; };
use reedline::{Completer as ReedlineCompleter, Suggestion}; use reedline::{Completer as ReedlineCompleter, Suggestion};
use std::{str, sync::Arc}; use std::str;
use std::sync::Arc;
use super::base::{SemanticSuggestion, SuggestionKind};
#[derive(Clone)] #[derive(Clone)]
pub struct NuCompleter { pub struct NuCompleter {
@ -22,17 +20,13 @@ pub struct NuCompleter {
} }
impl NuCompleter { impl NuCompleter {
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self { Self {
engine_state, engine_state,
stack: Stack::with_parent(stack).reset_out_dest().capture(), stack,
} }
} }
pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
self.completion_helper(line, pos)
}
// Process the completion for a given completer // Process the completion for a given completer
fn process_completion<T: Completer>( fn process_completion<T: Completer>(
&self, &self,
@ -42,25 +36,21 @@ impl NuCompleter {
new_span: Span, new_span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let config = self.engine_state.get_config(); let config = self.engine_state.get_config();
let options = CompletionOptions { let mut options = CompletionOptions {
case_sensitive: config.case_sensitive_completions, case_sensitive: config.case_sensitive_completions,
match_algorithm: config.completion_algorithm.into(),
..Default::default() ..Default::default()
}; };
if config.completion_algorithm == "fuzzy" {
options.match_algorithm = MatchAlgorithm::Fuzzy;
}
// Fetch // Fetch
let mut suggestions = completer.fetch( let mut suggestions =
working_set, completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options);
&self.stack,
prefix.clone(),
new_span,
offset,
pos,
&options,
);
// Sort // Sort
suggestions = completer.sort(suggestions, prefix); suggestions = completer.sort(suggestions, prefix);
@ -70,42 +60,44 @@ impl NuCompleter {
fn external_completion( fn external_completion(
&self, &self,
closure: &Closure, block_id: BlockId,
spans: &[String], spans: &[String],
offset: usize, offset: usize,
span: Span, span: Span,
) -> Option<Vec<SemanticSuggestion>> { ) -> Option<Vec<Suggestion>> {
let block = self.engine_state.get_block(closure.block_id); let stack = self.stack.clone();
let mut callee_stack = self let block = self.engine_state.get_block(block_id);
.stack let mut callee_stack = stack.gather_captures(&block.captures);
.captures_to_stack_preserve_out_dest(closure.captures.clone());
// Line // Line
if let Some(pos_arg) = block.signature.required_positional.first() { if let Some(pos_arg) = block.signature.required_positional.get(0) {
if let Some(var_id) = pos_arg.var_id { if let Some(var_id) = pos_arg.var_id {
callee_stack.add_var( callee_stack.add_var(
var_id, var_id,
Value::list( Value::List {
spans vals: spans
.iter() .iter()
.map(|it| Value::string(it, Span::unknown())) .map(|it| Value::string(it, Span::unknown()))
.collect(), .collect(),
Span::unknown(), span: Span::unknown(),
), },
); );
} }
} }
let result = eval_block::<WithoutDebug>( let result = eval_block(
&self.engine_state, &self.engine_state,
&mut callee_stack, &mut callee_stack,
block, block,
PipelineData::empty(), PipelineData::empty(),
true,
true,
); );
match result.and_then(|data| data.into_value(span)) { match result {
Ok(value) => { Ok(pd) => {
if let Value::List { vals, .. } = value { let value = pd.into_value(span);
if let Value::List { vals, span: _ } = value {
let result = let result =
map_value_completions(vals.iter(), Span::new(span.start, span.end), offset); map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
@ -118,258 +110,272 @@ impl NuCompleter {
None None
} }
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> { fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let mut working_set = StateWorkingSet::new(&self.engine_state); let mut working_set = StateWorkingSet::new(&self.engine_state);
let offset = working_set.next_span_start(); let offset = working_set.next_span_start();
// TODO: Callers should be trimming the line themselves let (mut new_line, alias_offset) = try_find_alias(line.as_bytes(), &working_set);
let line = if line.len() > pos { &line[..pos] } else { line };
// Adjust offset so that the spans of the suggestions will start at the right
// place even with `only_buffer_difference: true`
let fake_offset = offset + line.len() - pos;
let pos = offset + line.len();
let initial_line = line.to_string(); let initial_line = line.to_string();
let mut line = line.to_string(); let alias_total_offset: usize = alias_offset.iter().sum();
line.push('a'); new_line.insert(alias_total_offset + pos, b'a');
let pos = offset + pos;
let config = self.engine_state.get_config(); let config = self.engine_state.get_config();
let output = parse(&mut working_set, Some("completer"), line.as_bytes(), false); let (output, _err) = parse(&mut working_set, Some("completer"), &new_line, false, &[]);
for pipeline in &output.pipelines { for pipeline in output.pipelines.into_iter() {
for pipeline_element in &pipeline.elements { for pipeline_element in pipeline.elements {
let flattened = flatten_pipeline_element(&working_set, pipeline_element); match pipeline_element {
let mut spans: Vec<String> = vec![]; PipelineElement::Expression(_, expr)
| PipelineElement::Redirection(_, _, expr)
| PipelineElement::And(_, expr)
| PipelineElement::Or(_, expr)
| PipelineElement::SeparateRedirection { out: (_, expr), .. } => {
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
let span_offset: usize = alias_offset.iter().sum();
let mut spans: Vec<String> = vec![];
for (flat_idx, flat) in flattened.iter().enumerate() { for (flat_idx, flat) in flattened.iter().enumerate() {
let is_passthrough_command = spans let is_passthrough_command = spans
.first() .first()
.filter(|content| content.as_str() == "sudo" || content.as_str() == "doas") .filter(|content| *content == &String::from("sudo"))
.is_some(); .is_some();
// Read the current spam to string // Read the current spam to string
let current_span = working_set.get_span_contents(flat.0).to_vec(); let current_span = working_set.get_span_contents(flat.0).to_vec();
let current_span_str = String::from_utf8_lossy(&current_span); 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
if flat_idx == flattened.len() - 1 {
// Skip the last 'a' as span item let mut chars = current_span_str.chars();
if is_last_span { chars.next_back();
let offset = pos - flat.0.start; let current_span_str = chars.as_str().to_owned();
if offset == 0 { spans.push(current_span_str.to_string());
spans.push(String::new()) } else {
} else { spans.push(current_span_str.to_string());
let mut current_span_str = current_span_str.to_string();
current_span_str.remove(offset);
spans.push(current_span_str);
}
} else {
spans.push(current_span_str.to_string());
}
// 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(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).to_vec();
let index = pos - flat.0.start;
prefix.drain(index..);
// Variables completion
if prefix.starts_with(b"$") || most_left_var.is_some() {
let mut completer =
VariableCompletion::new(most_left_var.unwrap_or((vec![], vec![])));
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
// Flags completion
if prefix.starts_with(b"-") {
// Try to complete flag internally
let mut completer = FlagCompletion::new(pipeline_element.expr.clone());
let result = self.process_completion(
&mut completer,
&working_set,
prefix.clone(),
new_span,
fake_offset,
pos,
);
if !result.is_empty() {
return result;
} }
// We got no results for internal completion // Complete based on the last span
// now we can check if external completer is set and use it if pos + span_offset >= flat.0.start && pos + span_offset < flat.0.end {
if let Some(closure) = config.external_completer.as_ref() { // Context variables
if let Some(external_result) = let most_left_var =
self.external_completion(closure, &spans, fake_offset, new_span) most_left_variable(flat_idx, &working_set, flattened.clone());
{
return external_result;
}
}
}
// specially check if it is currently empty - always complete commands // Create a new span
if (is_passthrough_command && flat_idx == 1) let new_span = if flat_idx == 0 {
|| (flat_idx == 0 && working_set.get_span_contents(new_span).is_empty()) Span::new(flat.0.start, flat.0.end - 1 - span_offset)
{ } else {
let mut completer = CommandCompletion::new( Span::new(
flattened.clone(), flat.0.start - span_offset,
// flat_idx, flat.0.end - 1 - span_offset,
FlatShape::String, )
true, };
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
// Completions that depends on the previous expression (e.g: use, source-env) // Parses the prefix. Completion should look up to the cursor position, not after.
if (is_passthrough_command && flat_idx > 1) || flat_idx > 0 { let mut prefix = working_set.get_span_contents(flat.0).to_vec();
if let Some(previous_expr) = flattened.get(flat_idx - 1) { let index = pos - (flat.0.start - span_offset);
// Read the content for the previous expression prefix.drain(index..);
let prev_expr_str =
working_set.get_span_contents(previous_expr.0).to_vec();
// Completion for .nu files // Variables completion
if prev_expr_str == b"use" if prefix.starts_with(b"$") || most_left_var.is_some() {
|| prev_expr_str == b"overlay use" let mut completer = VariableCompletion::new(
|| prev_expr_str == b"source-env" self.engine_state.clone(),
{ self.stack.clone(),
let mut completer = DotNuCompletion::new(); most_left_var.unwrap_or((vec![], vec![])),
);
return self.process_completion( return self.process_completion(
&mut completer, &mut completer,
&working_set, &working_set,
prefix, prefix,
new_span, new_span,
fake_offset, offset,
pos, pos,
); );
} else if prev_expr_str == b"ls" { }
let mut completer = FileCompletion::new();
return self.process_completion( // Flags completion
if prefix.starts_with(b"-") {
// Try to complete flag internally
let mut completer = FlagCompletion::new(expr.clone());
let result = self.process_completion(
&mut completer, &mut completer,
&working_set, &working_set,
prefix, prefix.clone(),
new_span, new_span,
fake_offset, offset,
pos, pos,
); );
}
}
}
// Match other types if !result.is_empty() {
match &flat.1 { return result;
FlatShape::Custom(decl_id) => { }
let mut completer = CustomCompletion::new(
self.stack.clone(),
*decl_id,
initial_line,
);
return self.process_completion( // We got no results for internal completion
&mut completer, // now we can check if external completer is set and use it
&working_set, if let Some(block_id) = config.external_completer {
prefix, if let Some(external_result) = self
new_span, .external_completion(block_id, &spans, offset, new_span)
fake_offset, {
pos, return external_result;
); }
}
FlatShape::Directory => {
let mut completer = DirectoryCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
FlatShape::Filepath | FlatShape::GlobPattern => {
let mut completer = FileCompletion::new();
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
fake_offset,
pos,
);
}
flat_shape => {
let mut completer = CommandCompletion::new(
flattened.clone(),
// flat_idx,
flat_shape.clone(),
false,
);
let mut out: Vec<_> = self.process_completion(
&mut completer,
&working_set,
prefix.clone(),
new_span,
fake_offset,
pos,
);
if !out.is_empty() {
return out;
}
// Try to complete using an external completer (if set)
if let Some(closure) = config.external_completer.as_ref() {
if let Some(external_result) = self.external_completion(
closure,
&spans,
fake_offset,
new_span,
) {
return external_result;
} }
} }
// Check for file completion // specially check if it is currently empty - always complete commands
let mut completer = FileCompletion::new(); if (is_passthrough_command && flat_idx == 1)
out = self.process_completion( || (flat_idx == 0
&mut completer, && working_set.get_span_contents(new_span).is_empty())
&working_set, {
prefix, let mut completer = CommandCompletion::new(
new_span, self.engine_state.clone(),
fake_offset, &working_set,
pos, flattened.clone(),
); // flat_idx,
FlatShape::String,
if !out.is_empty() { true,
return out; );
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
} }
// Completions that depends on the previous expression (e.g: use, source-env)
if (is_passthrough_command && flat_idx > 1) || flat_idx > 0 {
if let Some(previous_expr) = flattened.get(flat_idx - 1) {
// Read the content for the previous expression
let prev_expr_str =
working_set.get_span_contents(previous_expr.0).to_vec();
// Completion for .nu files
if prev_expr_str == b"use" || prev_expr_str == b"source-env"
{
let mut completer =
DotNuCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
} else if prev_expr_str == b"ls" {
let mut completer =
FileCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
}
}
// Match other types
match &flat.1 {
FlatShape::Custom(decl_id) => {
let mut completer = CustomCompletion::new(
self.engine_state.clone(),
self.stack.clone(),
*decl_id,
initial_line,
);
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
FlatShape::Directory => {
let mut completer =
DirectoryCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
FlatShape::Filepath | FlatShape::GlobPattern => {
let mut completer =
FileCompletion::new(self.engine_state.clone());
return self.process_completion(
&mut completer,
&working_set,
prefix,
new_span,
offset,
pos,
);
}
flat_shape => {
let mut completer = CommandCompletion::new(
self.engine_state.clone(),
&working_set,
flattened.clone(),
// flat_idx,
flat_shape.clone(),
false,
);
let mut out: Vec<_> = self.process_completion(
&mut completer,
&working_set,
prefix.clone(),
new_span,
offset,
pos,
);
if !out.is_empty() {
return out;
}
// Try to complete using an external completer (if set)
if let Some(block_id) = config.external_completer {
if let Some(external_result) = self.external_completion(
block_id, &spans, offset, new_span,
) {
return external_result;
}
}
// 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;
}
}
};
} }
}; }
} }
} }
} }
@ -382,9 +388,85 @@ impl NuCompleter {
impl ReedlineCompleter for NuCompleter { impl ReedlineCompleter for NuCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> { fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
self.completion_helper(line, pos) self.completion_helper(line, pos)
.into_iter() }
.map(|s| s.suggestion) }
.collect()
type MatchedAlias = Vec<(Vec<u8>, Vec<u8>)>;
// Handler the completion when giving lines contains at least one alias. (e.g: `g checkout`)
// that `g` is an alias of `git`
fn try_find_alias(line: &[u8], working_set: &StateWorkingSet) -> (Vec<u8>, Vec<usize>) {
// An vector represents the offsets of alias
// e.g: the offset is 2 for the alias `g` of `git`
let mut alias_offset = vec![];
let mut output = vec![];
if let Some(matched_alias) = search_alias(line, working_set) {
let mut lens = matched_alias.len();
for (input_vec, line_vec) in matched_alias {
alias_offset.push(line_vec.len() - input_vec.len());
output.extend(line_vec);
if lens > 1 {
output.push(b' ');
lens -= 1;
}
}
if !line.is_empty() {
let last = line.last().expect("input is empty");
if last == &b' ' {
output.push(b' ');
}
}
} else {
output = line.to_vec();
}
(output, alias_offset)
}
fn search_alias(input: &[u8], working_set: &StateWorkingSet) -> Option<MatchedAlias> {
let mut vec_names = vec![];
let mut vec_alias = vec![];
let mut pos = 0;
let mut is_alias = false;
for (index, character) in input.iter().enumerate() {
if *character == b' ' {
let range = &input[pos..index];
vec_names.push(range.to_owned());
pos = index + 1;
}
}
// Push the rest to names vector.
if pos < input.len() {
vec_names.push(input[pos..].to_owned());
}
for name in &vec_names {
if let Some(alias_id) = working_set.find_alias(&name[..]) {
let alias_span = working_set.get_alias(alias_id);
let mut span_vec = vec![];
is_alias = true;
for alias in alias_span {
let name = working_set.get_span_contents(*alias);
if !name.is_empty() {
span_vec.push(name);
}
}
// Join span of vector together for complex alias, e.g: `f` is an alias for `git remote -v`
let full_aliases = span_vec.join(&[b' '][..]);
vec_alias.push(full_aliases);
} else {
vec_alias.push(name.to_owned());
}
}
if is_alias {
// Zip names and alias vectors, the original inputs and its aliases mapping.
// e.g:(['g'], ['g','i','t'])
let output = vec_names.into_iter().zip(vec_alias).collect();
Some(output)
} else {
None
} }
} }
@ -408,7 +490,7 @@ fn most_left_variable(
let result = working_set.get_span_contents(item.0).to_vec(); let result = working_set.get_span_contents(item.0).to_vec();
match item.1 { match item.1 {
FlatShape::Variable(_) => { FlatShape::Variable => {
variables_found.push(result); variables_found.push(result);
found_var = true; found_var = true;
@ -442,32 +524,27 @@ pub fn map_value_completions<'a>(
list: impl Iterator<Item = &'a Value>, list: impl Iterator<Item = &'a Value>,
span: Span, span: Span,
offset: usize, offset: usize,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
list.filter_map(move |x| { list.filter_map(move |x| {
// Match for string values // Match for string values
if let Ok(s) = x.coerce_string() { if let Ok(s) = x.as_string() {
return Some(SemanticSuggestion { return Some(Suggestion {
suggestion: Suggestion { value: s,
value: s, description: None,
description: None, extra: None,
style: None, span: reedline::Span {
extra: None, start: span.start - offset,
span: reedline::Span { end: span.end - offset,
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
}, },
kind: Some(SuggestionKind::Type(x.get_type())), append_whitespace: false,
}); });
} }
// Match for record values // Match for record values
if let Ok(record) = x.as_record() { if let Ok((cols, vals)) = x.as_record() {
let mut suggestion = Suggestion { let mut suggestion = Suggestion {
value: String::from(""), // Initialize with empty string value: String::from(""), // Initialize with empty string
description: None, description: None,
style: None,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: span.start - offset, start: span.start - offset,
@ -477,11 +554,11 @@ pub fn map_value_completions<'a>(
}; };
// Iterate the cols looking for `value` and `description` // Iterate the cols looking for `value` and `description`
record.iter().for_each(|it| { cols.iter().zip(vals).for_each(|it| {
// Match `value` column // Match `value` column
if it.0 == "value" { if it.0 == "value" {
// Convert the value to string // Convert the value to string
if let Ok(val_str) = it.1.coerce_string() { if let Ok(val_str) = it.1.as_string() {
// Update the suggestion value // Update the suggestion value
suggestion.value = val_str; suggestion.value = val_str;
} }
@ -490,27 +567,14 @@ pub fn map_value_completions<'a>(
// Match `description` column // Match `description` column
if it.0 == "description" { if it.0 == "description" {
// Convert the value to string // Convert the value to string
if let Ok(desc_str) = it.1.coerce_string() { if let Ok(desc_str) = it.1.as_string() {
// Update the suggestion value // Update the suggestion value
suggestion.description = Some(desc_str); suggestion.description = Some(desc_str);
} }
} }
// Match `style` column
if it.0 == "style" {
// Convert the value to string
suggestion.style = match it.1 {
Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
Value::Record { .. } => Some(color_record_to_nustyle(it.1)),
_ => None,
};
}
}); });
return Some(SemanticSuggestion { return Some(suggestion);
suggestion,
kind: Some(SuggestionKind::Type(x.get_type())),
});
} }
None None
@ -524,8 +588,7 @@ mod completer_tests {
#[test] #[test]
fn test_completion_helper() { fn test_completion_helper() {
let mut engine_state = let mut engine_state = nu_command::create_default_context();
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
// Custom additions // Custom additions
let delta = { let delta = {
@ -540,8 +603,8 @@ mod completer_tests {
result.err().unwrap() result.err().unwrap()
); );
let mut completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new())); let mut completer = NuCompleter::new(engine_state.into(), Stack::new());
let dataset = [ let dataset = vec![
("sudo", false, "", Vec::new()), ("sudo", false, "", Vec::new()),
("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]), ("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
(" sudo", false, "", Vec::new()), (" sudo", false, "", Vec::new()),
@ -562,13 +625,13 @@ mod completer_tests {
// Test whether the result begins with the expected value // Test whether the result begins with the expected value
result result
.iter() .iter()
.for_each(|x| assert!(x.suggestion.value.starts_with(begins_with))); .for_each(|x| assert!(x.value.starts_with(begins_with)));
// Test whether the result contains all the expected values // Test whether the result contains all the expected values
assert_eq!( assert_eq!(
result result
.iter() .iter()
.map(|x| expected_values.contains(&x.suggestion.value.as_str())) .map(|x| expected_values.contains(&x.value.as_str()))
.filter(|x| *x) .filter(|x| *x)
.count(), .count(),
expected_values.len(), expected_values.len(),

View File

@ -1,253 +0,0 @@
use crate::completions::{matches, CompletionOptions};
use nu_ansi_term::Style;
use nu_engine::env_to_string;
use nu_path::home_dir;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
Span,
};
use nu_utils::get_ls_colors;
use std::path::{
is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR,
};
#[derive(Clone, Default)]
pub struct PathBuiltFromString {
parts: Vec<String>,
isdir: bool,
}
fn complete_rec(
partial: &[&str],
built: &PathBuiltFromString,
cwd: &Path,
options: &CompletionOptions,
dir: bool,
isdir: bool,
) -> Vec<PathBuiltFromString> {
let mut completions = vec![];
if let Some((&base, rest)) = partial.split_first() {
if (base == "." || base == "..") && (isdir || !rest.is_empty()) {
let mut built = built.clone();
built.parts.push(base.to_string());
built.isdir = true;
return complete_rec(rest, &built, cwd, options, dir, isdir);
}
}
let mut built_path = cwd.to_path_buf();
for part in &built.parts {
built_path.push(part);
}
let Ok(result) = built_path.read_dir() else {
return completions;
};
for entry in result.filter_map(|e| e.ok()) {
let entry_name = entry.file_name().to_string_lossy().into_owned();
let entry_isdir = entry.path().is_dir();
let mut built = built.clone();
built.parts.push(entry_name.clone());
built.isdir = entry_isdir;
if !dir || entry_isdir {
match partial.split_first() {
Some((base, rest)) => {
if matches(base, &entry_name, options) {
if !rest.is_empty() || isdir {
completions
.extend(complete_rec(rest, &built, cwd, options, dir, isdir));
} else {
completions.push(built);
}
}
}
None => {
completions.push(built);
}
}
}
}
completions
}
#[derive(Debug)]
enum OriginalCwd {
None,
Home,
Prefix(String),
}
impl OriginalCwd {
fn apply(&self, mut p: PathBuiltFromString) -> String {
match self {
Self::None => {}
Self::Home => p.parts.insert(0, "~".to_string()),
Self::Prefix(s) => p.parts.insert(0, s.clone()),
};
let mut ret = p.parts.join(MAIN_SEPARATOR_STR);
if p.isdir {
ret.push(SEP);
}
ret
}
}
fn surround_remove(partial: &str) -> String {
for c in ['`', '"', '\''] {
if partial.starts_with(c) {
let ret = partial.strip_prefix(c).unwrap_or(partial);
return match ret.split(c).collect::<Vec<_>>()[..] {
[inside] => inside.to_string(),
[inside, outside] if inside.ends_with(is_separator) => format!("{inside}{outside}"),
_ => ret.to_string(),
};
}
}
partial.to_string()
}
pub fn complete_item(
want_directory: bool,
span: nu_protocol::Span,
partial: &str,
cwd: &str,
options: &CompletionOptions,
engine_state: &EngineState,
stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
let partial = surround_remove(partial);
let isdir = partial.ends_with(is_separator);
let cwd_pathbuf = Path::new(cwd).to_path_buf();
let ls_colors = (engine_state.config.use_ls_colors_completions
&& engine_state.config.use_ansi_coloring)
.then(|| {
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
Some(v) => env_to_string("LS_COLORS", &v, engine_state, stack).ok(),
None => None,
};
get_ls_colors(ls_colors_env_str)
});
let mut cwd = cwd_pathbuf.clone();
let mut prefix_len = 0;
let mut original_cwd = OriginalCwd::None;
let mut components = Path::new(&partial).components().peekable();
match components.peek().cloned() {
Some(c @ Component::Prefix(..)) => {
// windows only by definition
components.next();
if let Some(Component::RootDir) = components.peek().cloned() {
components.next();
};
cwd = [c, Component::RootDir].iter().collect();
prefix_len = c.as_os_str().len();
original_cwd = OriginalCwd::Prefix(c.as_os_str().to_string_lossy().into_owned());
}
Some(c @ Component::RootDir) => {
components.next();
// This is kind of a hack. When joining an empty string with the rest,
// we add the slash automagically
cwd = PathBuf::from(c.as_os_str());
prefix_len = 1;
original_cwd = OriginalCwd::Prefix(String::new());
}
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
components.next();
cwd = home_dir().unwrap_or(cwd_pathbuf);
prefix_len = 1;
original_cwd = OriginalCwd::Home;
}
_ => {}
};
let after_prefix = &partial[prefix_len..];
let partial: Vec<_> = after_prefix
.strip_prefix(is_separator)
.unwrap_or(after_prefix)
.split(is_separator)
.filter(|s| !s.is_empty())
.collect();
complete_rec(
partial.as_slice(),
&PathBuiltFromString::default(),
&cwd,
options,
want_directory,
isdir,
)
.into_iter()
.map(|p| {
let path = original_cwd.apply(p);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(&path, std::fs::symlink_metadata(&path).ok().as_ref())
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
});
(span, escape_path(path, want_directory), style)
})
.collect()
}
// Fix files or folders with quotes or hashes
pub fn escape_path(path: String, dir: bool) -> String {
// make glob pattern have the highest priority.
let glob_contaminated = path.contains(['[', '*', ']', '?']);
if glob_contaminated {
return if path.contains('\'') {
// decide to use double quote, also need to escape `"` in path
// or else users can't do anything with completed path either.
format!("\"{}\"", path.replace('"', r#"\""#))
} else {
format!("'{path}'")
};
}
let filename_contaminated = !dir && path.contains(['\'', '"', ' ', '#', '(', ')']);
let dirname_contaminated = dir && path.contains(['\'', '"', ' ', '#']);
let maybe_flag = path.starts_with('-');
let maybe_number = path.parse::<f64>().is_ok();
if filename_contaminated || dirname_contaminated || maybe_flag || maybe_number {
format!("`{path}`")
} else {
path
}
}
pub struct AdjustView {
pub prefix: String,
pub span: Span,
pub readjusted: bool,
}
pub fn adjust_if_intermediate(
prefix: &[u8],
working_set: &StateWorkingSet,
mut span: nu_protocol::Span,
) -> AdjustView {
let span_contents = String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
let mut prefix = String::from_utf8_lossy(prefix).to_string();
// A difference of 1 because of the cursor's unicode code point in between.
// Using .chars().count() because unicode and Windows.
let readjusted = span_contents.chars().count() - prefix.chars().count() > 1;
if readjusted {
let remnant: String = span_contents
.chars()
.skip(prefix.chars().count() + 1)
.take_while(|&c| !is_separator(c))
.collect();
prefix.push_str(&remnant);
span = Span::new(span.start, span.start + prefix.chars().count() + 1);
}
AdjustView {
prefix,
span,
readjusted,
}
}

View File

@ -1,7 +1,7 @@
use std::fmt::Display;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str; use nu_parser::trim_quotes_str;
use nu_protocol::CompletionAlgorithm;
use std::fmt::Display;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum SortBy { pub enum SortBy {
@ -55,15 +55,6 @@ impl MatchAlgorithm {
} }
} }
impl From<CompletionAlgorithm> for MatchAlgorithm {
fn from(value: CompletionAlgorithm) -> Self {
match value {
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
}
}
}
impl TryFrom<String> for MatchAlgorithm { impl TryFrom<String> for MatchAlgorithm {
type Error = InvalidMatchAlgorithm; type Error = InvalidMatchAlgorithm;
@ -95,6 +86,7 @@ impl std::error::Error for InvalidMatchAlgorithm {}
pub struct CompletionOptions { pub struct CompletionOptions {
pub case_sensitive: bool, pub case_sensitive: bool,
pub positional: bool, pub positional: bool,
pub sort_by: SortBy,
pub match_algorithm: MatchAlgorithm, pub match_algorithm: MatchAlgorithm,
} }
@ -103,6 +95,7 @@ impl Default for CompletionOptions {
Self { Self {
case_sensitive: true, case_sensitive: true,
positional: true, positional: true,
sort_by: SortBy::Ascending,
match_algorithm: MatchAlgorithm::Prefix, match_algorithm: MatchAlgorithm::Prefix,
} }
} }

View File

@ -1,18 +1,17 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy};
completer::map_value_completions, Completer, CompletionOptions, MatchAlgorithm,
SemanticSuggestion, SortBy,
};
use nu_engine::eval_call; use nu_engine::eval_call;
use nu_protocol::{ use nu_protocol::{
ast::{Argument, Call, Expr, Expression}, ast::{Argument, Call, Expr, Expression},
debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet},
engine::{Stack, StateWorkingSet},
PipelineData, Span, Type, Value, PipelineData, Span, Type, Value,
}; };
use nu_utils::IgnoreCaseExt; use reedline::Suggestion;
use std::collections::HashMap; use std::sync::Arc;
use super::completer::map_value_completions;
pub struct CustomCompletion { pub struct CustomCompletion {
engine_state: Arc<EngineState>,
stack: Stack, stack: Stack,
decl_id: usize, decl_id: usize,
line: String, line: String,
@ -20,8 +19,9 @@ pub struct CustomCompletion {
} }
impl CustomCompletion { impl CustomCompletion {
pub fn new(stack: Stack, decl_id: usize, line: String) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack, decl_id: usize, line: String) -> Self {
Self { Self {
engine_state,
stack, stack,
decl_id, decl_id,
line, line,
@ -33,20 +33,19 @@ impl CustomCompletion {
impl Completer for CustomCompletion { impl Completer for CustomCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
_stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
pos: usize, pos: usize,
completion_options: &CompletionOptions, completion_options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
// Line position // Line position
let line_pos = pos - offset; let line_pos = pos - offset;
// Call custom declaration // Call custom declaration
let result = eval_call::<WithoutDebug>( let result = eval_call(
working_set.permanent_state, &self.engine_state,
&mut self.stack, &mut self.stack,
&Call { &Call {
decl_id: self.decl_id, decl_id: self.decl_id,
@ -65,7 +64,9 @@ impl Completer for CustomCompletion {
custom_completion: None, custom_completion: None,
}), }),
], ],
parser_info: HashMap::new(), redirect_stdout: true,
redirect_stderr: true,
parser_info: vec![],
}, },
PipelineData::empty(), PipelineData::empty(),
); );
@ -74,53 +75,63 @@ impl Completer for CustomCompletion {
// Parse result // Parse result
let suggestions = result let suggestions = result
.and_then(|data| data.into_value(span)) .map(|pd| {
.map(|value| match &value { let value = pd.into_value(span);
Value::Record { val, .. } => { match &value {
let completions = val Value::Record { .. } => {
.get("completions") let completions = value
.and_then(|val| { .get_data_by_key("completions")
val.as_list() .and_then(|val| {
.ok() val.as_list()
.map(|it| map_value_completions(it.iter(), span, offset)) .ok()
}) .map(|it| map_value_completions(it.iter(), span, offset))
.unwrap_or_default(); })
let options = val.get("options"); .unwrap_or_default();
let options = value.get_data_by_key("options");
if let Some(Value::Record { val: options, .. }) = &options { if let Some(Value::Record { .. }) = &options {
let should_sort = options let options = options.unwrap_or_default();
.get("sort") let should_sort = options
.and_then(|val| val.as_bool().ok()) .get_data_by_key("sort")
.unwrap_or(false); .and_then(|val| val.as_bool().ok())
.unwrap_or(false);
if should_sort { if should_sort {
self.sort_by = SortBy::Ascending; self.sort_by = SortBy::Ascending;
}
custom_completion_options = Some(CompletionOptions {
case_sensitive: options
.get_data_by_key("case_sensitive")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
positional: options
.get_data_by_key("positional")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
sort_by: if should_sort {
SortBy::Ascending
} else {
SortBy::None
},
match_algorithm: match options
.get_data_by_key("completion_algorithm")
{
Some(option) => option
.as_string()
.ok()
.and_then(|option| option.try_into().ok())
.unwrap_or(MatchAlgorithm::Prefix),
None => completion_options.match_algorithm,
},
});
} }
custom_completion_options = Some(CompletionOptions { completions
case_sensitive: options
.get("case_sensitive")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
positional: options
.get("positional")
.and_then(|val| val.as_bool().ok())
.unwrap_or(true),
match_algorithm: match options.get("completion_algorithm") {
Some(option) => option
.coerce_string()
.ok()
.and_then(|option| option.try_into().ok())
.unwrap_or(MatchAlgorithm::Prefix),
None => completion_options.match_algorithm,
},
});
} }
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
completions _ => vec![],
} }
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
_ => vec![],
}) })
.unwrap_or_default(); .unwrap_or_default();
@ -136,23 +147,16 @@ impl Completer for CustomCompletion {
} }
} }
fn filter( fn filter(prefix: &[u8], items: Vec<Suggestion>, options: &CompletionOptions) -> Vec<Suggestion> {
prefix: &[u8],
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
items items
.into_iter() .into_iter()
.filter(|it| match options.match_algorithm { .filter(|it| match options.match_algorithm {
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) { MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix), (true, true) => it.value.as_bytes().starts_with(prefix),
(true, false) => it (true, false) => it.value.contains(std::str::from_utf8(prefix).unwrap_or("")),
.suggestion
.value
.contains(std::str::from_utf8(prefix).unwrap_or("")),
(false, positional) => { (false, positional) => {
let value = it.suggestion.value.to_folded_case(); let value = it.value.to_lowercase();
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case(); let prefix = std::str::from_utf8(prefix).unwrap_or("").to_lowercase();
if positional { if positional {
value.starts_with(&prefix) value.starts_with(&prefix)
} else { } else {
@ -162,7 +166,7 @@ fn filter(
}, },
MatchAlgorithm::Fuzzy => options MatchAlgorithm::Fuzzy => options
.match_algorithm .match_algorithm
.matches_u8(it.suggestion.value.as_bytes(), prefix), .matches_u8(it.value.as_bytes(), prefix),
}) })
.collect() .collect()
} }

View File

@ -1,72 +1,61 @@
use crate::completions::{ use crate::completions::{matches, Completer, CompletionOptions};
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, SortBy,
};
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, StateWorkingSet},
levenshtein_distance, Span, levenshtein_distance, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{Path, MAIN_SEPARATOR as SEP}; use std::fs;
use std::path::Path;
use std::sync::Arc;
use super::SemanticSuggestion; use super::{partial_from, prepend_base_dir, SortBy};
#[derive(Clone, Default)] const SEP: char = std::path::MAIN_SEPARATOR;
pub struct DirectoryCompletion {}
#[derive(Clone)]
pub struct DirectoryCompletion {
engine_state: Arc<EngineState>,
}
impl DirectoryCompletion { impl DirectoryCompletion {
pub fn new() -> Self { pub fn new(engine_state: Arc<EngineState>) -> Self {
Self::default() Self { engine_state }
} }
} }
impl Completer for DirectoryCompletion { impl Completer for DirectoryCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span); let cwd = self.engine_state.current_work_dir();
let partial = String::from_utf8_lossy(&prefix).to_string();
// Filter only the folders // Filter only the folders
#[allow(deprecated)] let output: Vec<_> = directory_completion(span, &partial, &cwd, options)
let output: Vec<_> = directory_completion( .into_iter()
span, .map(move |x| Suggestion {
&prefix,
&working_set.permanent_state.current_work_dir(),
options,
working_set.permanent_state,
stack,
)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1, value: x.1,
description: None, description: None,
style: x.2,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.0.start - offset,
end: x.0.end - offset, end: x.0.end - offset,
}, },
append_whitespace: false, append_whitespace: false,
}, })
// TODO???? .collect();
kind: None,
})
.collect();
output output
} }
// Sort results prioritizing the non hidden folders // Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> { fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string(); let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items // Sort items
@ -74,18 +63,12 @@ impl Completer for DirectoryCompletion {
match self.get_sort_by() { match self.get_sort_by() {
SortBy::Ascending => { SortBy::Ascending => {
sorted_items.sort_by(|a, b| { sorted_items.sort_by(|a, b| a.value.cmp(&b.value));
// Ignore trailing slashes in folder names when sorting
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
} }
SortBy::LevenshteinDistance => { SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| { sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); let b_distance = levenshtein_distance(&prefix_str, &b.value);
a_distance.cmp(&b_distance) a_distance.cmp(&b_distance)
}); });
} }
@ -93,11 +76,11 @@ impl Completer for DirectoryCompletion {
} }
// Separate the results between hidden and non hidden // Separate the results between hidden and non hidden
let mut hidden: Vec<SemanticSuggestion> = vec![]; let mut hidden: Vec<Suggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![]; let mut non_hidden: Vec<Suggestion> = vec![];
for item in sorted_items.into_iter() { for item in sorted_items.into_iter() {
let item_path = Path::new(&item.suggestion.value); let item_path = Path::new(&item.value);
if let Some(value) = item_path.file_name() { if let Some(value) = item_path.file_name() {
if let Some(value) = value.to_str() { if let Some(value) = value.to_str() {
@ -122,8 +105,61 @@ pub fn directory_completion(
partial: &str, partial: &str,
cwd: &str, cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, ) -> Vec<(nu_protocol::Span, String)> {
stack: &Stack, let original_input = partial;
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(true, span, partial, cwd, options, engine_state, stack) let (base_dir_name, partial) = partial_from(partial);
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
// which we don't want in this case (if we did, base_dir would already be ".")
if base_dir == Path::new("") {
return Vec::new();
}
if let Ok(result) = base_dir.read_dir() {
return result
.filter_map(|entry| {
entry.ok().and_then(|entry| {
if let Ok(metadata) = fs::metadata(entry.path()) {
if metadata.is_dir() {
let mut file_name = entry.file_name().to_string_lossy().into_owned();
if matches(&partial, &file_name, options) {
let mut path = if prepend_base_dir(original_input, &base_dir_name) {
format!("{base_dir_name}{file_name}")
} else {
file_name.to_string()
};
if entry.path().is_dir() {
path.push(SEP);
file_name.push(SEP);
}
// Fix files or folders with quotes or hash
if path.contains('\'')
|| path.contains('"')
|| path.contains(' ')
|| path.contains('#')
{
path = format!("`{path}`");
}
Some((span, path))
} else {
None
}
} else {
None
}
} else {
None
}
})
})
.collect();
}
Vec::new()
} }

View File

@ -1,69 +1,64 @@
use crate::completions::{file_path_completion, Completer, CompletionOptions, SortBy}; use crate::completions::{
file_path_completion, partial_from, Completer, CompletionOptions, SortBy,
};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{EngineState, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}; use std::sync::Arc;
const SEP: char = std::path::MAIN_SEPARATOR;
use super::SemanticSuggestion; #[derive(Clone)]
pub struct DotNuCompletion {
#[derive(Clone, Default)] engine_state: Arc<EngineState>,
pub struct DotNuCompletion {} }
impl DotNuCompletion { impl DotNuCompletion {
pub fn new() -> Self { pub fn new(engine_state: Arc<EngineState>) -> Self {
Self::default() Self { engine_state }
} }
} }
impl Completer for DotNuCompletion { impl Completer for DotNuCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).replace('`', ""); let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut search_dirs: Vec<String> = vec![]; let mut search_dirs: Vec<String> = vec![];
let (base_dir, mut partial) = partial_from(&prefix_str);
// If prefix_str is only a word we want to search in the current dir
let (base, partial) = prefix_str
.rsplit_once(is_separator)
.unwrap_or((".", &prefix_str));
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
let mut partial = partial.to_string();
// On windows, this standardizes paths to use \
let mut is_current_folder = false; let mut is_current_folder = false;
// Fetch the lib dirs // Fetch the lib dirs
let lib_dirs: Vec<String> = if let Some(lib_dirs) = working_set.get_env_var("NU_LIB_DIRS") { let lib_dirs: Vec<String> =
lib_dirs if let Some(lib_dirs) = self.engine_state.get_env_var("NU_LIB_DIRS") {
.as_list() lib_dirs
.into_iter() .as_list()
.flat_map(|it| { .into_iter()
it.iter().map(|x| { .flat_map(|it| {
x.to_path() it.iter().map(|x| {
.expect("internal error: failed to convert lib path") x.as_path()
.expect("internal error: failed to convert lib path")
})
}) })
}) .map(|it| {
.map(|it| { it.into_os_string()
it.into_os_string() .into_string()
.into_string() .expect("internal error: failed to convert OS path")
.expect("internal error: failed to convert OS path") })
}) .collect()
.collect() } else {
} else { vec![]
vec![] };
};
// Check if the base_dir is a folder // Check if the base_dir is a folder
// rsplit_once removes the separator if base_dir != format!(".{SEP}") {
if base_dir != "." {
// Add the base dir into the directories to be searched // Add the base dir into the directories to be searched
search_dirs.push(base_dir.clone()); search_dirs.push(base_dir.clone());
@ -75,8 +70,7 @@ impl Completer for DotNuCompletion {
partial = base_dir_partial; partial = base_dir_partial;
} else { } else {
// Fetch the current folder // Fetch the current folder
#[allow(deprecated)] let current_folder = self.engine_state.current_work_dir();
let current_folder = working_set.permanent_state.current_work_dir();
is_current_folder = true; is_current_folder = true;
// Add the current folder and the lib dirs into the // Add the current folder and the lib dirs into the
@ -87,46 +81,29 @@ impl Completer for DotNuCompletion {
// Fetch the files filtering the ones that ends with .nu // Fetch the files filtering the ones that ends with .nu
// and transform them into suggestions // and transform them into suggestions
let output: Vec<SemanticSuggestion> = search_dirs let output: Vec<Suggestion> = search_dirs
.into_iter() .into_iter()
.flat_map(|search_dir| { .flat_map(|it| {
let completions = file_path_completion( file_path_completion(span, &partial, &it, options)
span,
&partial,
&search_dir,
options,
working_set.permanent_state,
stack,
);
completions
.into_iter() .into_iter()
.filter(move |it| { .filter(|it| {
// Different base dir, so we list the .nu files or folders // Different base dir, so we list the .nu files or folders
if !is_current_folder { if !is_current_folder {
it.1.ends_with(".nu") || it.1.ends_with(SEP) it.1.ends_with(".nu") || it.1.ends_with(SEP)
} else { } else {
// Lib dirs, so we filter only the .nu files or directory modules // Lib dirs, so we filter only the .nu files
if it.1.ends_with(SEP) { it.1.ends_with(".nu")
Path::new(&search_dir).join(&it.1).join("mod.nu").exists()
} else {
it.1.ends_with(".nu")
}
} }
}) })
.map(move |x| SemanticSuggestion { .map(move |x| Suggestion {
suggestion: Suggestion { value: x.1,
value: x.1, description: None,
description: None, extra: None,
style: x.2, span: reedline::Span {
extra: None, start: x.0.start - offset,
span: reedline::Span { end: x.0.end - offset,
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: true,
}, },
// TODO???? append_whitespace: true,
kind: None,
}) })
}) })
.collect(); .collect();

View File

@ -1,77 +1,58 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions};
completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, SortBy,
};
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, StateWorkingSet},
levenshtein_distance, Span, levenshtein_distance, Span,
}; };
use nu_utils::IgnoreCaseExt;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{Path, MAIN_SEPARATOR as SEP}; use std::path::{is_separator, Path};
use std::sync::Arc;
use super::SemanticSuggestion; use super::SortBy;
#[derive(Clone, Default)] const SEP: char = std::path::MAIN_SEPARATOR;
pub struct FileCompletion {}
#[derive(Clone)]
pub struct FileCompletion {
engine_state: Arc<EngineState>,
}
impl FileCompletion { impl FileCompletion {
pub fn new() -> Self { pub fn new(engine_state: Arc<EngineState>) -> Self {
Self::default() Self { engine_state }
} }
} }
impl Completer for FileCompletion { impl Completer for FileCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, _: &StateWorkingSet,
stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let AdjustView { let cwd = self.engine_state.current_work_dir();
prefix, let prefix = String::from_utf8_lossy(&prefix).to_string();
span, let output: Vec<_> = file_path_completion(span, &prefix, &cwd, options)
readjusted, .into_iter()
} = adjust_if_intermediate(&prefix, working_set, span); .map(move |x| Suggestion {
#[allow(deprecated)]
let output: Vec<_> = complete_item(
readjusted,
span,
&prefix,
&working_set.permanent_state.current_work_dir(),
options,
working_set.permanent_state,
stack,
)
.into_iter()
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1, value: x.1,
description: None, description: None,
style: x.2,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.0.start - offset,
end: x.0.end - offset, end: x.0.end - offset,
}, },
append_whitespace: false, append_whitespace: false,
}, })
// TODO???? .collect();
kind: None,
})
.collect();
output output
} }
// Sort results prioritizing the non hidden folders // Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> { fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string(); let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items // Sort items
@ -79,18 +60,12 @@ impl Completer for FileCompletion {
match self.get_sort_by() { match self.get_sort_by() {
SortBy::Ascending => { SortBy::Ascending => {
sorted_items.sort_by(|a, b| { sorted_items.sort_by(|a, b| a.value.cmp(&b.value));
// Ignore trailing slashes in folder names when sorting
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
} }
SortBy::LevenshteinDistance => { SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| { sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value); let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value); let b_distance = levenshtein_distance(&prefix_str, &b.value);
a_distance.cmp(&b_distance) a_distance.cmp(&b_distance)
}); });
} }
@ -98,11 +73,11 @@ impl Completer for FileCompletion {
} }
// Separate the results between hidden and non hidden // Separate the results between hidden and non hidden
let mut hidden: Vec<SemanticSuggestion> = vec![]; let mut hidden: Vec<Suggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![]; let mut non_hidden: Vec<Suggestion> = vec![];
for item in sorted_items.into_iter() { for item in sorted_items.into_iter() {
let item_path = Path::new(&item.suggestion.value); let item_path = Path::new(&item.value);
if let Some(value) = item_path.file_name() { if let Some(value) = item_path.file_name() {
if let Some(value) = value.to_str() { if let Some(value) = value.to_str() {
@ -122,15 +97,84 @@ impl Completer for FileCompletion {
} }
} }
pub fn partial_from(input: &str) -> (String, String) {
let partial = input.replace('`', "");
// If partial is only a word we want to search in the current dir
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
// On windows, this standardizes paths to use \
let mut base = base.replace(is_separator, &SEP.to_string());
// rsplit_once removes the separator
base.push(SEP);
(base.to_string(), rest.to_string())
}
pub fn file_path_completion( pub fn file_path_completion(
span: nu_protocol::Span, span: nu_protocol::Span,
partial: &str, partial: &str,
cwd: &str, cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
engine_state: &EngineState, ) -> Vec<(nu_protocol::Span, String)> {
stack: &Stack, let original_input = partial;
) -> Vec<(nu_protocol::Span, String, Option<Style>)> { let (base_dir_name, partial) = partial_from(partial);
complete_item(false, span, partial, cwd, options, engine_state, stack)
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
// which we don't want in this case (if we did, base_dir would already be ".")
if base_dir == Path::new("") {
return Vec::new();
}
if let Ok(result) = base_dir.read_dir() {
return result
.filter_map(|entry| {
entry.ok().and_then(|entry| {
let mut file_name = entry.file_name().to_string_lossy().into_owned();
if matches(&partial, &file_name, options) {
let mut path = if prepend_base_dir(original_input, &base_dir_name) {
format!("{base_dir_name}{file_name}")
} else {
file_name.to_string()
};
if entry.path().is_dir() {
path.push(SEP);
file_name.push(SEP);
}
// Fix files or folders with quotes or hashes
if path.contains('\'')
|| path.contains('"')
|| path.contains(' ')
|| path.contains('#')
|| path.contains('(')
|| path.contains(')')
|| path.starts_with('0')
|| path.starts_with('1')
|| path.starts_with('2')
|| path.starts_with('3')
|| path.starts_with('4')
|| path.starts_with('5')
|| path.starts_with('6')
|| path.starts_with('7')
|| path.starts_with('8')
|| path.starts_with('9')
{
path = format!("`{path}`");
}
Some((span, path))
} else {
None
}
})
})
.collect();
}
Vec::new()
} }
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool { pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
@ -138,8 +182,28 @@ pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {
if !options.case_sensitive { if !options.case_sensitive {
return options return options
.match_algorithm .match_algorithm
.matches_str(&from.to_folded_case(), &partial.to_folded_case()); .matches_str(&from.to_ascii_lowercase(), &partial.to_ascii_lowercase());
} }
options.match_algorithm.matches_str(from, partial) options.match_algorithm.matches_str(from, partial)
} }
/// Returns whether the base_dir should be prepended to the file path
pub fn prepend_base_dir(input: &str, base_dir: &str) -> bool {
if base_dir == format!(".{SEP}") {
// if the current base_dir path is the local folder we only add a "./" prefix if the user
// input already includes a local folder prefix.
let manually_entered = {
let mut chars = input.chars();
let first_char = chars.next();
let second_char = chars.next();
first_char == Some('.') && second_char.map(is_separator).unwrap_or(false)
};
manually_entered
} else {
// always prepend the base dir if it is a subfolder
true
}
}

View File

@ -1,12 +1,11 @@
use crate::completions::{Completer, CompletionOptions}; use crate::completions::{Completer, CompletionOptions};
use nu_protocol::{ use nu_protocol::{
ast::{Expr, Expression}, ast::{Expr, Expression},
engine::{Stack, StateWorkingSet}, engine::StateWorkingSet,
Span, Span,
}; };
use reedline::Suggestion;
use super::SemanticSuggestion; use reedline::Suggestion;
#[derive(Clone)] #[derive(Clone)]
pub struct FlagCompletion { pub struct FlagCompletion {
@ -23,13 +22,12 @@ impl Completer for FlagCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
_stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
// Check if it's a flag // Check if it's a flag
if let Expr::Call(call) = &self.expression.expr { if let Expr::Call(call) = &self.expression.expr {
let decl = working_set.get_decl(call.decl_id); let decl = working_set.get_decl(call.decl_id);
@ -45,20 +43,15 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) { if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: String::from_utf8_lossy(&named).to_string(),
value: String::from_utf8_lossy(&named).to_string(), description: Some(flag_desc.to_string()),
description: Some(flag_desc.to_string()), extra: None,
style: None, span: reedline::Span {
extra: None, start: span.start - offset,
span: reedline::Span { end: span.end - offset,
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
}, },
// TODO???? append_whitespace: true,
kind: None,
}); });
} }
} }
@ -72,20 +65,15 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) { if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: String::from_utf8_lossy(&named).to_string(),
value: String::from_utf8_lossy(&named).to_string(), description: Some(flag_desc.to_string()),
description: Some(flag_desc.to_string()), extra: None,
style: None, span: reedline::Span {
extra: None, start: span.start - offset,
span: reedline::Span { end: span.end - offset,
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
}, },
// TODO???? append_whitespace: true,
kind: None,
}); });
} }
} }

View File

@ -1,7 +1,6 @@
mod base; mod base;
mod command_completions; mod command_completions;
mod completer; mod completer;
mod completion_common;
mod completion_options; mod completion_options;
mod custom_completions; mod custom_completions;
mod directory_completions; mod directory_completions;
@ -10,13 +9,15 @@ mod file_completions;
mod flag_completions; mod flag_completions;
mod variable_completions; mod variable_completions;
pub use base::{Completer, SemanticSuggestion, SuggestionKind}; pub use base::Completer;
pub use command_completions::CommandCompletion; pub use command_completions::CommandCompletion;
pub use completer::NuCompleter; pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy}; pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};
pub use custom_completions::CustomCompletion; pub use custom_completions::CustomCompletion;
pub use directory_completions::DirectoryCompletion; pub use directory_completions::DirectoryCompletion;
pub use dotnu_completions::DotNuCompletion; pub use dotnu_completions::DotNuCompletion;
pub use file_completions::{file_path_completion, matches, FileCompletion}; pub use file_completions::{
file_path_completion, matches, partial_from, prepend_base_dir, FileCompletion,
};
pub use flag_completions::FlagCompletion; pub use flag_completions::FlagCompletion;
pub use variable_completions::VariableCompletion; pub use variable_completions::VariableCompletion;

View File

@ -1,22 +1,34 @@
use crate::completions::{ use crate::completions::{Completer, CompletionOptions};
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion, SuggestionKind, use nu_engine::eval_variable;
};
use nu_engine::{column::get_columns, eval_variable};
use nu_protocol::{ use nu_protocol::{
engine::{Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Value, Span, Value,
}; };
use reedline::Suggestion; use reedline::Suggestion;
use std::str; use std::str;
use std::sync::Arc;
use super::MatchAlgorithm;
#[derive(Clone)] #[derive(Clone)]
pub struct VariableCompletion { pub struct VariableCompletion {
engine_state: Arc<EngineState>, // TODO: Is engine state necessary? It's already a part of working set in fetch()
stack: Stack,
var_context: (Vec<u8>, Vec<Vec<u8>>), // tuple with $var and the sublevels (.b.c.d) var_context: (Vec<u8>, Vec<Vec<u8>>), // tuple with $var and the sublevels (.b.c.d)
} }
impl VariableCompletion { impl VariableCompletion {
pub fn new(var_context: (Vec<u8>, Vec<Vec<u8>>)) -> Self { pub fn new(
Self { var_context } engine_state: Arc<EngineState>,
stack: Stack,
var_context: (Vec<u8>, Vec<Vec<u8>>),
) -> Self {
Self {
engine_state,
stack,
var_context,
}
} }
} }
@ -24,16 +36,17 @@ impl Completer for VariableCompletion {
fn fetch( fn fetch(
&mut self, &mut self,
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
stack: &Stack,
prefix: Vec<u8>, prefix: Vec<u8>,
span: Span, span: Span,
offset: usize, offset: usize,
_pos: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let mut output = vec![]; let mut output = vec![];
let builtins = ["$nu", "$in", "$env"]; let builtins = ["$nu", "$in", "$env", "$nothing"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or(""); let var_str = std::str::from_utf8(&self.var_context.0)
.unwrap_or("")
.to_lowercase();
let var_id = working_set.find_variable(&self.var_context.0); let var_id = working_set.find_variable(&self.var_context.0);
let current_span = reedline::Span { let current_span = reedline::Span {
start: span.start - offset, start: span.start - offset,
@ -44,8 +57,8 @@ impl Completer for VariableCompletion {
// Completions for the given variable // Completions for the given variable
if !var_str.is_empty() { if !var_str.is_empty() {
// Completion for $env.<tab> // Completion for $env.<tab>
if var_str == "$env" { if var_str.as_str() == "$env" {
let env_vars = stack.get_env_vars(working_set.permanent_state); let env_vars = self.stack.get_env_vars(&self.engine_state);
// Return nested values // Return nested values
if sublevels_count > 0 { if sublevels_count > 0 {
@ -59,10 +72,12 @@ impl Completer for VariableCompletion {
self.var_context.1.clone().into_iter().skip(1).collect(); self.var_context.1.clone().into_iter().skip(1).collect();
if let Some(val) = env_vars.get(&target_var_str) { if let Some(val) = env_vars.get(&target_var_str) {
for suggestion in nested_suggestions(val, &nested_levels, current_span) { for suggestion in
nested_suggestions(val.clone(), nested_levels, current_span)
{
if options.match_algorithm.matches_u8_insensitive( if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive, options.case_sensitive,
suggestion.suggestion.value.as_bytes(), suggestion.value.as_bytes(),
&prefix, &prefix,
) { ) {
output.push(suggestion); output.push(suggestion);
@ -79,16 +94,12 @@ impl Completer for VariableCompletion {
env_var.0.as_bytes(), env_var.0.as_bytes(),
&prefix, &prefix,
) { ) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: env_var.0,
value: env_var.0, description: None,
description: None, extra: None,
style: None, span: current_span,
extra: None, append_whitespace: false,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
}); });
} }
} }
@ -98,19 +109,20 @@ impl Completer for VariableCompletion {
} }
// Completions for $nu.<tab> // Completions for $nu.<tab>
if var_str == "$nu" { if var_str.as_str() == "$nu" {
// Eval nu var // Eval nu var
if let Ok(nuval) = eval_variable( if let Ok(nuval) = eval_variable(
working_set.permanent_state, &self.engine_state,
stack, &self.stack,
nu_protocol::NU_VARIABLE_ID, nu_protocol::NU_VARIABLE_ID,
nu_protocol::Span::new(current_span.start, current_span.end), nu_protocol::Span::new(current_span.start, current_span.end),
) { ) {
for suggestion in nested_suggestions(&nuval, &self.var_context.1, current_span) for suggestion in
nested_suggestions(nuval, self.var_context.1.clone(), current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive, options.case_sensitive,
suggestion.suggestion.value.as_bytes(), suggestion.value.as_bytes(),
&prefix, &prefix,
) { ) {
output.push(suggestion); output.push(suggestion);
@ -124,15 +136,16 @@ impl Completer for VariableCompletion {
// Completion other variable types // Completion other variable types
if let Some(var_id) = var_id { if let Some(var_id) = var_id {
// Extract the variable value from the stack // Extract the variable value from the stack
let var = stack.get_var(var_id, Span::new(span.start, span.end)); let var = self.stack.get_var(var_id, Span::new(span.start, span.end));
// If the value exists and it's of type Record // If the value exists and it's of type Record
if let Ok(value) = var { if let Ok(value) = var {
for suggestion in nested_suggestions(&value, &self.var_context.1, current_span) for suggestion in
nested_suggestions(value, self.var_context.1.clone(), current_span)
{ {
if options.match_algorithm.matches_u8_insensitive( if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive, options.case_sensitive,
suggestion.suggestion.value.as_bytes(), suggestion.value.as_bytes(),
&prefix, &prefix,
) { ) {
output.push(suggestion); output.push(suggestion);
@ -151,17 +164,12 @@ impl Completer for VariableCompletion {
builtin.as_bytes(), builtin.as_bytes(),
&prefix, &prefix,
) { ) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: builtin.to_string(),
value: builtin.to_string(), description: None,
description: None, extra: None,
style: None, span: current_span,
extra: None, append_whitespace: false,
span: current_span,
append_whitespace: false,
},
// TODO is there a way to get the VarId to get the type???
kind: None,
}); });
} }
} }
@ -171,25 +179,23 @@ impl Completer for VariableCompletion {
let mut removed_overlays = vec![]; let mut removed_overlays = vec![];
// Working set scope vars // Working set scope vars
for scope_frame in working_set.delta.scope.iter().rev() { for scope_frame in working_set.delta.scope.iter().rev() {
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() { for overlay_frame in scope_frame
.active_overlays(&mut removed_overlays)
.iter()
.rev()
{
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
if options.match_algorithm.matches_u8_insensitive( if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive, options.case_sensitive,
v.0, v.0,
&prefix, &prefix,
) { ) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: String::from_utf8_lossy(v.0).to_string(),
value: String::from_utf8_lossy(v.0).to_string(), description: None,
description: None, extra: None,
style: None, span: current_span,
extra: None, append_whitespace: false,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
}); });
} }
} }
@ -198,9 +204,10 @@ impl Completer for VariableCompletion {
// Permanent state vars // Permanent state vars
// for scope in &self.engine_state.scope { // for scope in &self.engine_state.scope {
for overlay_frame in working_set for overlay_frame in self
.permanent_state .engine_state
.active_overlays(&removed_overlays) .active_overlays(&removed_overlays)
.iter()
.rev() .rev()
{ {
for v in &overlay_frame.vars { for v in &overlay_frame.vars {
@ -209,18 +216,12 @@ impl Completer for VariableCompletion {
v.0, v.0,
&prefix, &prefix,
) { ) {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: String::from_utf8_lossy(v.0).to_string(),
value: String::from_utf8_lossy(v.0).to_string(), description: None,
description: None, extra: None,
style: None, span: current_span,
extra: None, append_whitespace: false,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
}); });
} }
} }
@ -235,85 +236,79 @@ impl Completer for VariableCompletion {
// Find recursively the values for sublevels // Find recursively the values for sublevels
// if no sublevels are set it returns the current value // if no sublevels are set it returns the current value
fn nested_suggestions( fn nested_suggestions(
val: &Value, val: Value,
sublevels: &[Vec<u8>], sublevels: Vec<Vec<u8>>,
current_span: reedline::Span, current_span: reedline::Span,
) -> Vec<SemanticSuggestion> { ) -> Vec<Suggestion> {
let mut output: Vec<SemanticSuggestion> = vec![]; let mut output: Vec<Suggestion> = vec![];
let value = recursive_value(val, sublevels).unwrap_or_else(Value::nothing); let value = recursive_value(val, sublevels);
let kind = SuggestionKind::Type(value.get_type());
match value { match value {
Value::Record { val, .. } => { Value::Record {
cols,
vals: _,
span: _,
} => {
// Add all the columns as completion // Add all the columns as completion
for col in val.columns() { for item in cols {
output.push(SemanticSuggestion { output.push(Suggestion {
suggestion: Suggestion { value: item,
value: col.clone(), description: None,
description: None, extra: None,
style: None, span: current_span,
extra: None, append_whitespace: false,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
}); });
} }
output output
} }
Value::List { vals, .. } => { Value::LazyRecord { val, .. } => {
for column_name in get_columns(vals.as_slice()) { // Add all the columns as completion
output.push(SemanticSuggestion { for column_name in val.column_names() {
suggestion: Suggestion { output.push(Suggestion {
value: column_name, value: column_name.to_string(),
description: None, description: None,
style: None, extra: None,
extra: None, span: current_span,
span: current_span, append_whitespace: false,
append_whitespace: false,
},
kind: Some(kind.clone()),
}); });
} }
output output
} }
_ => output, _ => output,
} }
} }
// Extracts the recursive value (e.g: $var.a.b.c) // Extracts the recursive value (e.g: $var.a.b.c)
fn recursive_value(val: &Value, sublevels: &[Vec<u8>]) -> Result<Value, Span> { fn recursive_value(val: Value, sublevels: Vec<Vec<u8>>) -> Value {
// Go to next sublevel // Go to next sublevel
if let Some((sublevel, next_sublevels)) = sublevels.split_first() { if let Some(next_sublevel) = sublevels.clone().into_iter().next() {
let span = val.span();
match val { match val {
Value::Record { val, .. } => { Value::Record {
if let Some((_, value)) = val.iter().find(|(key, _)| key.as_bytes() == sublevel) { cols,
// If matches try to fetch recursively the next vals,
recursive_value(value, next_sublevels) span: _,
} else { } => {
// Current sublevel value not found for item in cols.into_iter().zip(vals.into_iter()) {
Err(span) // Check if index matches with sublevel
} if item.0.as_bytes().to_vec() == next_sublevel {
} // If matches try to fetch recursively the next
Value::List { vals, .. } => { return recursive_value(item.1, sublevels.into_iter().skip(1).collect());
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 // Current sublevel value not found
Err(span) return Value::Nothing {
span: Span::unknown(),
};
} }
_ => Ok(val.clone()), _ => return val,
} }
} else {
Ok(val.clone())
} }
val
} }
impl MatchAlgorithm { impl MatchAlgorithm {

View File

@ -1,20 +1,18 @@
use crate::util::eval_source; use crate::util::{eval_source, report_error};
#[cfg(feature = "plugin")]
use nu_parser::ParseError;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_protocol::{engine::StateWorkingSet, report_error, ParseError, PluginRegistryFile, Spanned}; use nu_protocol::Spanned;
use nu_protocol::{ use nu_protocol::{HistoryFileFormat, PipelineData};
engine::{EngineState, Stack},
report_error_new, HistoryFileFormat, PipelineData,
};
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_utils::utils::perf; use nu_utils::utils::perf;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
const PLUGIN_FILE: &str = "plugin.msgpackz"; const PLUGIN_FILE: &str = "plugin.nu";
#[cfg(feature = "plugin")]
const OLD_PLUGIN_FILE: &str = "plugin.nu";
const HISTORY_FILE_TXT: &str = "history.txt"; const HISTORY_FILE_TXT: &str = "history.txt";
const HISTORY_FILE_SQLITE: &str = "history.sqlite3"; const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
@ -22,149 +20,40 @@ const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub fn read_plugin_file( pub fn read_plugin_file(
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack,
plugin_file: Option<Spanned<String>>, plugin_file: Option<Spanned<String>>,
storage_path: &str, storage_path: &str,
) { ) {
use nu_protocol::ShellError; let start_time = std::time::Instant::now();
use std::path::Path; let mut plug_path = String::new();
// Reading signatures from signature file
// The plugin.nu file stores the parsed signature collected from each registered plugin
add_plugin_file(engine_state, plugin_file, storage_path);
let span = plugin_file.as_ref().map(|s| s.span); let plugin_path = engine_state.plugin_signatures.clone();
if let Some(plugin_path) = plugin_path {
// Check and warn + abort if this is a .nu plugin file let plugin_filename = plugin_path.to_string_lossy();
if plugin_file plug_path = plugin_filename.to_string();
.as_ref() if let Ok(contents) = std::fs::read(&plugin_path) {
.and_then(|p| Path::new(&p.item).extension()) eval_source(
.is_some_and(|ext| ext == "nu") engine_state,
{ stack,
report_error_new( &contents,
engine_state, &plugin_filename,
&ShellError::GenericError { PipelineData::empty(),
error: "Wrong plugin file format".into(), false,
msg: ".nu plugin files are no longer supported".into(), );
span, }
help: Some("please recreate this file in the new .msgpackz format".into()),
inner: vec![],
},
);
return;
} }
let mut start_time = std::time::Instant::now();
// Reading signatures from plugin registry file
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
add_plugin_file(engine_state, plugin_file.clone(), storage_path);
perf( perf(
"add plugin file to engine_state", &format!("read_plugin_file {}", &plug_path),
start_time, start_time,
file!(), file!(),
line!(), line!(),
column!(), column!(),
engine_state.get_config().use_ansi_coloring, engine_state.get_config().use_ansi_coloring,
); );
start_time = std::time::Instant::now();
let plugin_path = engine_state.plugin_path.clone();
if let Some(plugin_path) = plugin_path {
// Open the plugin file
let mut file = match std::fs::File::open(&plugin_path) {
Ok(file) => file,
Err(err) => {
if err.kind() == std::io::ErrorKind::NotFound {
log::warn!("Plugin file not found: {}", plugin_path.display());
// Try migration of an old plugin file if this wasn't a custom plugin file
if plugin_file.is_none() && migrate_old_plugin_file(engine_state, storage_path)
{
let Ok(file) = std::fs::File::open(&plugin_path) else {
log::warn!("Failed to load newly migrated plugin file");
return;
};
file
} else {
return;
}
} else {
report_error_new(
engine_state,
&ShellError::GenericError {
error: format!(
"Error while opening plugin registry file: {}",
plugin_path.display()
),
msg: "plugin path defined here".into(),
span,
help: None,
inner: vec![err.into()],
},
);
return;
}
}
};
// Abort if the file is empty.
if file.metadata().is_ok_and(|m| m.len() == 0) {
log::warn!(
"Not reading plugin file because it's empty: {}",
plugin_path.display()
);
return;
}
// Read the contents of the plugin file
let contents = match PluginRegistryFile::read_from(&mut file, span) {
Ok(contents) => contents,
Err(err) => {
log::warn!("Failed to read plugin registry file: {err:?}");
report_error_new(
engine_state,
&ShellError::GenericError {
error: format!(
"Error while reading plugin registry file: {}",
plugin_path.display()
),
msg: "plugin path defined here".into(),
span,
help: Some(
"you might try deleting the file and registering all of your \
plugins again"
.into(),
),
inner: vec![],
},
);
return;
}
};
perf(
&format!("read plugin file {}", plugin_path.display()),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
);
start_time = std::time::Instant::now();
let mut working_set = StateWorkingSet::new(engine_state);
nu_plugin_engine::load_plugin_file(&mut working_set, &contents, span);
if let Err(err) = engine_state.merge_delta(working_set.render()) {
report_error_new(engine_state, &err);
return;
}
perf(
&format!("load plugin file {}", plugin_path.display()),
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
);
}
} }
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
@ -173,39 +62,21 @@ pub fn add_plugin_file(
plugin_file: Option<Spanned<String>>, plugin_file: Option<Spanned<String>>,
storage_path: &str, storage_path: &str,
) { ) {
use std::path::Path; if let Some(plugin_file) = plugin_file {
let working_set = StateWorkingSet::new(engine_state);
let cwd = working_set.get_cwd();
let working_set = StateWorkingSet::new(engine_state); if let Ok(path) = canonicalize_with(&plugin_file.item, cwd) {
engine_state.plugin_signatures = Some(path)
if let Ok(cwd) = engine_state.cwd_as_string(None) { } else {
if let Some(plugin_file) = plugin_file { let e = ParseError::FileNotFound(plugin_file.item, plugin_file.span);
let path = Path::new(&plugin_file.item); report_error(&working_set, &e);
let path_dir = path.parent().unwrap_or(path);
// Just try to canonicalize the directory of the plugin file first.
if let Ok(path_dir) = canonicalize_with(path_dir, &cwd) {
// Try to canonicalize the actual filename, but it's ok if that fails. The file doesn't
// have to exist.
let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
let path = canonicalize_with(&path, &cwd).unwrap_or(path);
engine_state.plugin_path = Some(path)
} else {
// It's an error if the directory for the plugin file doesn't exist.
report_error(
&working_set,
&ParseError::FileNotFound(
path_dir.to_string_lossy().into_owned(),
plugin_file.span,
),
);
}
} else if let Some(mut plugin_path) = nu_path::config_dir() {
// Path to store plugins signatures
plugin_path.push(storage_path);
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
plugin_path.push(PLUGIN_FILE);
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
engine_state.plugin_path = Some(plugin_path);
} }
} else if let Some(mut plugin_path) = nu_path::config_dir() {
// Path to store plugins signatures
plugin_path.push(storage_path);
plugin_path.push(PLUGIN_FILE);
engine_state.plugin_signatures = Some(plugin_path.clone());
} }
} }
@ -218,10 +89,6 @@ pub fn eval_config_contents(
let config_filename = config_path.to_string_lossy(); let config_filename = config_path.to_string_lossy();
if let Ok(contents) = std::fs::read(&config_path) { if let Ok(contents) = std::fs::read(&config_path) {
// Set the current active file to the config file.
let prev_file = engine_state.file.take();
engine_state.file = Some(config_path.clone());
eval_source( eval_source(
engine_state, engine_state,
stack, stack,
@ -231,18 +98,17 @@ pub fn eval_config_contents(
false, false,
); );
// Restore the current active file.
engine_state.file = prev_file;
// Merge the environment in case env vars changed in the config // Merge the environment in case env vars changed in the config
match engine_state.cwd(Some(stack)) { match nu_engine::env::current_dir(engine_state, stack) {
Ok(cwd) => { Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) { if let Err(e) = engine_state.merge_env(stack, cwd) {
report_error_new(engine_state, &e); let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
} }
} }
Err(e) => { Err(e) => {
report_error_new(engine_state, &e); let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
} }
} }
} }
@ -259,132 +125,3 @@ pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> O
history_path history_path
}) })
} }
#[cfg(feature = "plugin")]
pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -> bool {
use nu_protocol::{
PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
ShellError,
};
use std::collections::BTreeMap;
let start_time = std::time::Instant::now();
let Ok(cwd) = engine_state.cwd_as_string(None) else {
return false;
};
let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| {
dir.push(storage_path);
nu_path::canonicalize_with(dir, &cwd).ok()
}) else {
return false;
};
let Ok(old_plugin_file_path) = nu_path::canonicalize_with(OLD_PLUGIN_FILE, &config_dir) else {
return false;
};
let old_contents = match std::fs::read(&old_plugin_file_path) {
Ok(old_contents) => old_contents,
Err(err) => {
report_error_new(
engine_state,
&ShellError::GenericError {
error: "Can't read old plugin file to migrate".into(),
msg: "".into(),
span: None,
help: Some(err.to_string()),
inner: vec![],
},
);
return false;
}
};
// Make a copy of the engine state, because we'll read the newly generated file
let mut engine_state = engine_state.clone();
let mut stack = Stack::new();
if eval_source(
&mut engine_state,
&mut stack,
&old_contents,
&old_plugin_file_path.to_string_lossy(),
PipelineData::Empty,
false,
) != 0
{
return false;
}
// Now that the plugin commands are loaded, we just have to generate the file
let mut contents = PluginRegistryFile::new();
let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
for decl in engine_state.plugin_decls() {
if let Some(identity) = decl.plugin_identity() {
groups
.entry(identity.clone())
.or_default()
.push(PluginSignature {
sig: decl.signature(),
examples: decl
.examples()
.into_iter()
.map(PluginExample::from)
.collect(),
})
}
}
for (identity, commands) in groups {
contents.upsert_plugin(PluginRegistryItem {
name: identity.name().to_owned(),
filename: identity.filename().to_owned(),
shell: identity.shell().map(|p| p.to_owned()),
data: PluginRegistryItemData::Valid { commands },
});
}
// Write the new file
let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
if let Err(err) = std::fs::File::create(&new_plugin_file_path)
.map_err(|e| e.into())
.and_then(|file| contents.write_to(file, None))
{
report_error_new(
&engine_state,
&ShellError::GenericError {
error: "Failed to save migrated plugin file".into(),
msg: "".into(),
span: None,
help: Some("ensure `$nu.plugin-path` is writable".into()),
inner: vec![err],
},
);
return false;
}
if engine_state.is_interactive {
eprintln!(
"Your old plugin.nu file has been migrated to the new format: {}",
new_plugin_file_path.display()
);
eprintln!(
"The plugin.nu file has not been removed. If `plugin list` looks okay, \
you may do so manually."
);
}
perf(
"migrate old plugin file",
start_time,
file!(),
line!(),
column!(),
engine_state.get_config().use_ansi_coloring,
);
true
}

View File

@ -1,70 +0,0 @@
use log::info;
use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse;
use nu_protocol::{
debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet},
report_error, PipelineData, ShellError, Spanned, Value,
};
use std::sync::Arc;
/// Run a command (or commands) given to us by the user
pub fn evaluate_commands(
commands: &Spanned<String>,
engine_state: &mut EngineState,
stack: &mut Stack,
input: PipelineData,
table_mode: Option<Value>,
no_newline: bool,
) -> Result<(), ShellError> {
// Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?;
// Parse the source code
let (block, delta) = {
if let Some(ref t_mode) = table_mode {
let mut config = engine_state.get_config().clone();
config.table_mode = t_mode.coerce_str()?.parse().unwrap_or_default();
engine_state.set_config(config);
}
let mut working_set = StateWorkingSet::new(engine_state);
let output = parse(&mut working_set, None, commands.item.as_bytes(), false);
if let Some(warning) = working_set.parse_warnings.first() {
report_error(&working_set, warning);
}
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
std::process::exit(1);
}
(output, working_set.render())
};
// Update permanent state
engine_state.merge_delta(delta)?;
// Run the block
let pipeline = eval_block::<WithoutDebug>(engine_state, stack, &block, input)?;
if let PipelineData::Value(Value::Error { error, .. }, ..) = pipeline {
return Err(*error);
}
if let Some(t_mode) = table_mode {
Arc::make_mut(&mut engine_state.config).table_mode =
t_mode.coerce_str()?.parse().unwrap_or_default();
}
if let Some(status) = pipeline.print(engine_state, stack, no_newline, false)? {
if status.code() != 0 {
std::process::exit(status.code())
}
}
info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(())
}

View File

@ -1,140 +1,204 @@
use crate::util::eval_source; use crate::util::{eval_source, report_error};
use log::{info, trace}; use log::info;
use nu_engine::{convert_env_values, eval_block}; use log::trace;
use miette::{IntoDiagnostic, Result};
use nu_engine::{convert_env_values, current_dir};
use nu_parser::parse; use nu_parser::parse;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug, ast::Call,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
report_error, PipelineData, ShellError, Span, Value, Config, PipelineData, ShellError, Span, Type, Value,
}; };
use std::sync::Arc; use nu_utils::stdout_write_all_and_flush;
/// Entry point for evaluating a file. /// Main function used when a file path is found as argument for nu
///
/// If the file contains a main command, it is invoked with `args` and the pipeline data from `input`;
/// otherwise, the pipeline data is forwarded to the first command in the file, and `args` are ignored.
pub fn evaluate_file( pub fn evaluate_file(
path: String, path: String,
args: &[String], args: &[String],
engine_state: &mut EngineState, engine_state: &mut EngineState,
stack: &mut Stack, stack: &mut Stack,
input: PipelineData, input: PipelineData,
) -> Result<(), ShellError> { ) -> Result<()> {
// Convert environment variables from Strings to Values and store them in the engine state. // Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?; if let Some(e) = convert_env_values(engine_state, stack) {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
std::process::exit(1);
}
let cwd = engine_state.cwd_as_string(Some(stack))?; let cwd = current_dir(engine_state, stack)?;
let file_path = let file_path = canonicalize_with(&path, cwd).unwrap_or_else(|e| {
canonicalize_with(&path, cwd).map_err(|err| ShellError::FileNotFoundCustom { let working_set = StateWorkingSet::new(engine_state);
msg: format!("Could not access file '{path}': {err}"), report_error(
span: Span::unknown(), &working_set,
})?; &ShellError::FileNotFoundCustom(
format!("Could not access file '{}': {:?}", path, e.to_string()),
let file_path_str = file_path Span::unknown(),
.to_str()
.ok_or_else(|| ShellError::NonUtf8Custom {
msg: format!(
"Input file name '{}' is not valid UTF8",
file_path.to_string_lossy()
), ),
span: Span::unknown(), );
})?; std::process::exit(1);
});
let file = std::fs::read(&file_path).map_err(|err| ShellError::FileNotFoundCustom { let file_path_str = file_path.to_str().unwrap_or_else(|| {
msg: format!("Could not read file '{file_path_str}': {err}"), let working_set = StateWorkingSet::new(engine_state);
span: Span::unknown(), report_error(
})?; &working_set,
engine_state.file = Some(file_path.clone()); &ShellError::NonUtf8Custom(
format!(
"Input file name '{}' is not valid UTF8",
file_path.to_string_lossy()
),
Span::unknown(),
),
);
std::process::exit(1);
});
let parent = file_path let file = std::fs::read(&file_path)
.parent() .into_diagnostic()
.ok_or_else(|| ShellError::FileNotFoundCustom { .unwrap_or_else(|e| {
msg: format!("The file path '{file_path_str}' does not have a parent"), let working_set = StateWorkingSet::new(engine_state);
span: Span::unknown(), report_error(
})?; &working_set,
&ShellError::FileNotFoundCustom(
format!(
"Could not read file '{}': {:?}",
file_path_str,
e.to_string()
),
Span::unknown(),
),
);
std::process::exit(1);
});
engine_state.start_in_file(Some(file_path_str));
let parent = file_path.parent().unwrap_or_else(|| {
let working_set = StateWorkingSet::new(engine_state);
report_error(
&working_set,
&ShellError::FileNotFoundCustom(
format!("The file path '{file_path_str}' does not have a parent"),
Span::unknown(),
),
);
std::process::exit(1);
});
stack.add_env_var( stack.add_env_var(
"FILE_PWD".to_string(), "FILE_PWD".to_string(),
Value::string(parent.to_string_lossy(), Span::unknown()), Value::string(parent.to_string_lossy(), Span::unknown()),
); );
stack.add_env_var(
"CURRENT_FILE".to_string(),
Value::string(file_path.to_string_lossy(), Span::unknown()),
);
stack.add_env_var(
"PROCESS_PATH".to_string(),
Value::string(path, Span::unknown()),
);
let source_filename = file_path
.file_name()
.expect("internal error: missing filename");
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
trace!("parsing file: {}", file_path_str); trace!("parsing file: {}", file_path_str);
let block = parse(&mut working_set, Some(file_path_str), &file, false); let _ = parse(&mut working_set, Some(file_path_str), &file, false, &[]);
// If any parse errors were found, report the first error and exit. if working_set.find_decl(b"main", &Type::Any).is_some() {
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
std::process::exit(1);
}
// Look for blocks whose name starts with "main" and replace it with the filename.
for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) {
if block.signature.name == "main" {
block.signature.name = source_filename.to_string_lossy().to_string();
} else if block.signature.name.starts_with("main ") {
block.signature.name =
source_filename.to_string_lossy().to_string() + " " + &block.signature.name[5..];
}
}
// Merge the changes into the engine state.
engine_state.merge_delta(working_set.delta)?;
// Check if the file contains a main command.
let exit_code = if engine_state.find_decl(b"main", &[]).is_some() {
// Evaluate the file, but don't run main yet.
let pipeline =
match eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty()) {
Ok(data) => data,
Err(ShellError::Return { .. }) => {
// Allow early return before main is run.
return Ok(());
}
Err(err) => return Err(err),
};
// Print the pipeline output of the last command of the file.
if let Some(status) = pipeline.print(engine_state, stack, true, false)? {
if status.code() != 0 {
std::process::exit(status.code())
}
}
// Invoke the main command with arguments.
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
let args = format!("main {}", args.join(" ")); let args = format!("main {}", args.join(" "));
eval_source(
if !eval_source(
engine_state,
stack,
&file,
file_path_str,
PipelineData::empty(),
true,
) {
std::process::exit(1);
}
if !eval_source(
engine_state, engine_state,
stack, stack,
args.as_bytes(), args.as_bytes(),
"<commandline>", "<commandline>",
input, input,
true, true,
) ) {
} else { std::process::exit(1);
eval_source(engine_state, stack, &file, file_path_str, input, true) }
}; } else if !eval_source(engine_state, stack, &file, file_path_str, input, true) {
std::process::exit(1);
if exit_code != 0 {
std::process::exit(exit_code)
} }
info!("evaluate {}:{}:{}", file!(), line!(), column!()); info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(()) Ok(())
} }
pub(crate) fn print_table_or_error(
engine_state: &mut EngineState,
stack: &mut Stack,
mut pipeline_data: PipelineData,
config: &mut Config,
) -> Option<i64> {
let exit_code = match &mut pipeline_data {
PipelineData::ExternalStream { exit_code, .. } => exit_code.take(),
_ => None,
};
// Change the engine_state config to use the passed in configuration
engine_state.set_config(config);
if let PipelineData::Value(Value::Error { error }, ..) = &pipeline_data {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &**error);
std::process::exit(1);
}
if let Some(decl_id) = engine_state.find_decl("table".as_bytes(), &[]) {
let command = engine_state.get_decl(decl_id);
if command.get_block_id().is_some() {
print_or_exit(pipeline_data, engine_state, config);
} else {
// The final call on table command, it's ok to set redirect_output to false.
let mut call = Call::new(Span::new(0, 0));
call.redirect_stdout = false;
let table = command.run(engine_state, stack, &call, pipeline_data);
match table {
Ok(table) => {
print_or_exit(table, engine_state, config);
}
Err(error) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &error);
std::process::exit(1);
}
}
}
} else {
print_or_exit(pipeline_data, engine_state, config);
}
// Make sure everything has finished
if let Some(exit_code) = exit_code {
let mut exit_code: Vec<_> = exit_code.into_iter().collect();
exit_code
.pop()
.and_then(|last_exit_code| match last_exit_code {
Value::Int { val: code, .. } => Some(code),
_ => None,
})
} else {
None
}
}
fn print_or_exit(pipeline_data: PipelineData, engine_state: &mut EngineState, config: &Config) {
for item in pipeline_data {
if let Value::Error { error } = item {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &*error);
std::process::exit(1);
}
let out = item.into_string("\n", config) + "\n";
let _ = stdout_write_all_and_flush(out).map_err(|err| eprintln!("{err}"));
}
}

View File

@ -1,7 +1,6 @@
mod commands; mod commands;
mod completions; mod completions;
mod config_files; mod config_files;
mod eval_cmds;
mod eval_file; mod eval_file;
mod menus; mod menus;
mod nu_highlight; mod nu_highlight;
@ -14,24 +13,21 @@ mod syntax_highlight;
mod util; mod util;
mod validation; mod validation;
pub use commands::add_cli_context; pub use commands::evaluate_commands;
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind}; pub use completions::{FileCompletion, NuCompleter};
pub use config_files::eval_config_contents; pub use config_files::eval_config_contents;
pub use eval_cmds::evaluate_commands;
pub use eval_file::evaluate_file; pub use eval_file::evaluate_file;
pub use menus::NuHelpCompleter; pub use menus::{DescriptionMenu, NuHelpCompleter};
pub use nu_cmd_base::util::get_init_cwd;
pub use nu_highlight::NuHighlight; pub use nu_highlight::NuHighlight;
pub use print::Print; pub use print::Print;
pub use prompt::NushellPrompt; pub use prompt::NushellPrompt;
pub use repl::evaluate_repl; pub use repl::evaluate_repl;
pub use repl::{eval_env_change_hook, eval_hook};
pub use syntax_highlight::NuHighlighter; pub use syntax_highlight::NuHighlighter;
pub use util::{eval_source, gather_parent_env_vars}; pub use util::{eval_source, gather_parent_env_vars, get_init_cwd, report_error, report_error_new};
pub use validation::NuValidator; pub use validation::NuValidator;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub use config_files::add_plugin_file; pub use config_files::add_plugin_file;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
pub use config_files::migrate_old_plugin_file;
#[cfg(feature = "plugin")]
pub use config_files::read_plugin_file; pub use config_files::read_plugin_file;

View File

@ -0,0 +1,727 @@
use {
nu_ansi_term::{ansi::RESET, Style},
reedline::{
menu_functions::string_difference, Completer, Editor, Menu, MenuEvent, MenuTextStyle,
Painter, Suggestion, UndoBehavior,
},
};
/// Default values used as reference for the menu. These values are set during
/// the initial declaration of the menu and are always kept as reference for the
/// changeable [`WorkingDetails`]
struct DefaultMenuDetails {
/// Number of columns that the menu will have
pub columns: u16,
/// Column width
pub col_width: Option<usize>,
/// Column padding
pub col_padding: usize,
/// Number of rows for commands
pub selection_rows: u16,
/// Number of rows allowed to display the description
pub description_rows: usize,
}
impl Default for DefaultMenuDetails {
fn default() -> Self {
Self {
columns: 4,
col_width: None,
col_padding: 2,
selection_rows: 4,
description_rows: 10,
}
}
}
/// Represents the actual column conditions of the menu. These conditions change
/// since they need to accommodate possible different line sizes for the column values
#[derive(Default)]
struct WorkingDetails {
/// Number of columns that the menu will have
pub columns: u16,
/// Column width
pub col_width: usize,
/// Number of rows for description
pub description_rows: usize,
}
/// Completion menu definition
pub struct DescriptionMenu {
/// Menu name
name: String,
/// Menu status
active: bool,
/// Menu coloring
color: MenuTextStyle,
/// Default column details that are set when creating the menu
/// These values are the reference for the working details
default_details: DefaultMenuDetails,
/// Number of minimum rows that are displayed when
/// the required lines is larger than the available lines
min_rows: u16,
/// Working column details keep changing based on the collected values
working_details: WorkingDetails,
/// Menu cached values
values: Vec<Suggestion>,
/// column position of the cursor. Starts from 0
col_pos: u16,
/// row position in the menu. Starts from 0
row_pos: u16,
/// Menu marker when active
marker: String,
/// Event sent to the menu
event: Option<MenuEvent>,
/// String collected after the menu is activated
input: Option<String>,
/// Examples to select
examples: Vec<String>,
/// Example index
example_index: Option<usize>,
/// Examples may not be shown if there is not enough space in the screen
show_examples: bool,
/// Skipped description rows
skipped_rows: usize,
/// Calls the completer using only the line buffer difference difference
/// after the menu was activated
only_buffer_difference: bool,
}
impl Default for DescriptionMenu {
fn default() -> Self {
Self {
name: "description_menu".to_string(),
active: false,
color: MenuTextStyle::default(),
default_details: DefaultMenuDetails::default(),
min_rows: 3,
working_details: WorkingDetails::default(),
values: Vec::new(),
col_pos: 0,
row_pos: 0,
marker: "? ".to_string(),
event: None,
input: None,
examples: Vec::new(),
example_index: None,
show_examples: true,
skipped_rows: 0,
only_buffer_difference: true,
}
}
}
// Menu configuration
impl DescriptionMenu {
/// Menu builder with new name
pub fn with_name(mut self, name: &str) -> Self {
self.name = name.into();
self
}
/// Menu builder with new value for text style
pub fn with_text_style(mut self, text_style: Style) -> Self {
self.color.text_style = text_style;
self
}
/// Menu builder with new value for text style
pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self {
self.color.selected_text_style = selected_text_style;
self
}
/// Menu builder with new value for text style
pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
self.color.description_style = description_text_style;
self
}
/// Menu builder with new columns value
pub fn with_columns(mut self, columns: u16) -> Self {
self.default_details.columns = columns;
self
}
/// Menu builder with new column width value
pub fn with_column_width(mut self, col_width: Option<usize>) -> Self {
self.default_details.col_width = col_width;
self
}
/// Menu builder with new column width value
pub fn with_column_padding(mut self, col_padding: usize) -> Self {
self.default_details.col_padding = col_padding;
self
}
/// Menu builder with new selection rows value
pub fn with_selection_rows(mut self, selection_rows: u16) -> Self {
self.default_details.selection_rows = selection_rows;
self
}
/// Menu builder with new description rows value
pub fn with_description_rows(mut self, description_rows: usize) -> Self {
self.default_details.description_rows = description_rows;
self
}
/// Menu builder with marker
pub fn with_marker(mut self, marker: String) -> Self {
self.marker = marker;
self
}
/// Menu builder with new only buffer difference
pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
self.only_buffer_difference = only_buffer_difference;
self
}
}
// Menu functionality
impl DescriptionMenu {
/// Move menu cursor to the next element
fn move_next(&mut self) {
let mut new_col = self.col_pos + 1;
let mut new_row = self.row_pos;
if new_col >= self.get_cols() {
new_row += 1;
new_col = 0;
}
if new_row >= self.get_rows() {
new_row = 0;
new_col = 0;
}
let position = new_row * self.get_cols() + new_col;
if position >= self.get_values().len() as u16 {
self.reset_position();
} else {
self.col_pos = new_col;
self.row_pos = new_row;
}
}
/// Move menu cursor to the previous element
fn move_previous(&mut self) {
let new_col = self.col_pos.checked_sub(1);
let (new_col, new_row) = match new_col {
Some(col) => (col, self.row_pos),
None => match self.row_pos.checked_sub(1) {
Some(row) => (self.get_cols().saturating_sub(1), row),
None => (
self.get_cols().saturating_sub(1),
self.get_rows().saturating_sub(1),
),
},
};
let position = new_row * self.get_cols() + new_col;
if position >= self.get_values().len() as u16 {
self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1);
self.row_pos = self.get_rows().saturating_sub(1);
} else {
self.col_pos = new_col;
self.row_pos = new_row;
}
}
/// Menu index based on column and row position
fn index(&self) -> usize {
let index = self.row_pos * self.get_cols() + self.col_pos;
index as usize
}
/// Get selected value from the menu
fn get_value(&self) -> Option<Suggestion> {
self.get_values().get(self.index()).cloned()
}
/// Calculates how many rows the Menu will use
fn get_rows(&self) -> u16 {
let values = self.get_values().len() as u16;
if values == 0 {
// When the values are empty the no_records_msg is shown, taking 1 line
return 1;
}
let rows = values / self.get_cols();
if values % self.get_cols() != 0 {
rows + 1
} else {
rows
}
}
/// Returns working details col width
fn get_width(&self) -> usize {
self.working_details.col_width
}
/// Reset menu position
fn reset_position(&mut self) {
self.col_pos = 0;
self.row_pos = 0;
self.skipped_rows = 0;
}
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
let msg = "TYPE TO START SEARCH";
if use_ansi_coloring {
format!(
"{}{}{}",
self.color.selected_text_style.prefix(),
msg,
RESET
)
} else {
msg.to_string()
}
}
/// Returns working details columns
fn get_cols(&self) -> u16 {
self.working_details.columns.max(1)
}
/// End of line for menu
fn end_of_line(&self, column: u16, index: usize) -> &str {
let is_last = index == self.values.len().saturating_sub(1);
if column == self.get_cols().saturating_sub(1) || is_last {
"\r\n"
} else {
""
}
}
/// Update list of examples from the actual value
fn update_examples(&mut self) {
self.examples = self
.get_value()
.and_then(|suggestion| suggestion.extra)
.unwrap_or_default();
self.example_index = None;
}
/// Creates default string that represents one suggestion from the menu
fn create_entry_string(
&self,
suggestion: &Suggestion,
index: usize,
column: u16,
empty_space: usize,
use_ansi_coloring: bool,
) -> String {
if use_ansi_coloring {
if index == self.index() {
format!(
"{}{}{}{:>empty$}{}",
self.color.selected_text_style.prefix(),
&suggestion.value,
RESET,
"",
self.end_of_line(column, index),
empty = empty_space,
)
} else {
format!(
"{}{}{}{:>empty$}{}",
self.color.text_style.prefix(),
&suggestion.value,
RESET,
"",
self.end_of_line(column, index),
empty = empty_space,
)
}
} else {
// If no ansi coloring is found, then the selection word is
// the line in uppercase
let (marker, empty_space) = if index == self.index() {
(">", empty_space.saturating_sub(1))
} else {
("", empty_space)
};
let line = format!(
"{}{}{:>empty$}{}",
marker,
&suggestion.value,
"",
self.end_of_line(column, index),
empty = empty_space,
);
if index == self.index() {
line.to_uppercase()
} else {
line
}
}
}
/// Description string with color
fn create_description_string(&self, use_ansi_coloring: bool) -> String {
let description = self
.get_value()
.and_then(|suggestion| suggestion.description)
.unwrap_or_default()
.lines()
.skip(self.skipped_rows)
.take(self.working_details.description_rows)
.collect::<Vec<&str>>()
.join("\r\n");
if use_ansi_coloring && !description.is_empty() {
format!(
"{}{}{}",
self.color.description_style.prefix(),
description,
RESET,
)
} else {
description
}
}
/// Selectable list of examples from the actual value
fn create_example_string(&self, use_ansi_coloring: bool) -> String {
if !self.show_examples {
return "".into();
}
let examples: String = self
.examples
.iter()
.enumerate()
.map(|(index, example)| {
if let Some(example_index) = self.example_index {
if index == example_index {
format!(
" {}{}{}\r\n",
self.color.selected_text_style.prefix(),
example,
RESET
)
} else {
format!(" {example}\r\n")
}
} else {
format!(" {example}\r\n")
}
})
.collect();
if examples.is_empty() {
"".into()
} else if use_ansi_coloring {
format!(
"{}\r\n\r\nExamples:\r\n{}{}",
self.color.description_style.prefix(),
RESET,
examples,
)
} else {
format!("\r\n\r\nExamples:\r\n{examples}",)
}
}
}
impl Menu for DescriptionMenu {
/// Menu name
fn name(&self) -> &str {
self.name.as_str()
}
/// Menu indicator
fn indicator(&self) -> &str {
self.marker.as_str()
}
/// Deactivates context menu
fn is_active(&self) -> bool {
self.active
}
/// The menu stays active even with one record
fn can_quick_complete(&self) -> bool {
false
}
/// The menu does not need to partially complete
fn can_partially_complete(
&mut self,
_values_updated: bool,
_editor: &mut Editor,
_completer: &mut dyn Completer,
) -> bool {
false
}
/// Selects what type of event happened with the menu
fn menu_event(&mut self, event: MenuEvent) {
match &event {
MenuEvent::Activate(_) => self.active = true,
MenuEvent::Deactivate => {
self.active = false;
self.input = None;
self.values = Vec::new();
}
_ => {}
};
self.event = Some(event);
}
/// Updates menu values
fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
if self.only_buffer_difference {
if let Some(old_string) = &self.input {
let (start, input) = string_difference(editor.get_buffer(), old_string);
if !input.is_empty() {
self.reset_position();
self.values = completer.complete(input, start);
}
}
} else {
let trimmed_buffer = editor.get_buffer().replace('\n', " ");
self.values = completer.complete(
trimmed_buffer.as_str(),
editor.line_buffer().insertion_point(),
);
self.reset_position();
}
}
/// The working details for the menu changes based on the size of the lines
/// collected from the completer
fn update_working_details(
&mut self,
editor: &mut Editor,
completer: &mut dyn Completer,
painter: &Painter,
) {
if let Some(event) = self.event.take() {
// Updating all working parameters from the menu before executing any of the
// possible event
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
let str_len = suggestion.value.len() + self.default_details.col_padding;
if str_len > acc {
str_len
} else {
acc
}
});
// If no default width is found, then the total screen width is used to estimate
// the column width based on the default number of columns
let default_width = if let Some(col_width) = self.default_details.col_width {
col_width
} else {
let col_width = painter.screen_width() / self.default_details.columns;
col_width as usize
};
// Adjusting the working width of the column based the max line width found
// in the menu values
if max_width > default_width {
self.working_details.col_width = max_width;
} else {
self.working_details.col_width = default_width;
};
// The working columns is adjusted based on possible number of columns
// that could be fitted in the screen with the calculated column width
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
if possible_cols > self.default_details.columns {
self.working_details.columns = self.default_details.columns.max(1);
} else {
self.working_details.columns = possible_cols;
}
// Updating the working rows to display the description
if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() {
self.working_details.description_rows = self.default_details.description_rows;
self.show_examples = true;
} else {
self.working_details.description_rows = painter
.remaining_lines()
.saturating_sub(self.default_details.selection_rows + 1)
as usize;
self.show_examples = false;
}
match event {
MenuEvent::Activate(_) => {
self.reset_position();
self.input = Some(editor.get_buffer().to_string());
self.update_values(editor, completer);
}
MenuEvent::Deactivate => self.active = false,
MenuEvent::Edit(_) => {
self.reset_position();
self.update_values(editor, completer);
self.update_examples()
}
MenuEvent::NextElement => {
self.skipped_rows = 0;
self.move_next();
self.update_examples();
}
MenuEvent::PreviousElement => {
self.skipped_rows = 0;
self.move_previous();
self.update_examples();
}
MenuEvent::MoveUp => {
if let Some(example_index) = self.example_index {
if let Some(index) = example_index.checked_sub(1) {
self.example_index = Some(index);
} else {
self.example_index = Some(self.examples.len().saturating_sub(1));
}
} else if !self.examples.is_empty() {
self.example_index = Some(0);
}
}
MenuEvent::MoveDown => {
if let Some(example_index) = self.example_index {
let index = example_index + 1;
if index < self.examples.len() {
self.example_index = Some(index);
} else {
self.example_index = Some(0);
}
} else if !self.examples.is_empty() {
self.example_index = Some(0);
}
}
MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1),
MenuEvent::MoveRight => {
let skipped = self.skipped_rows + 1;
let description_rows = self
.get_value()
.and_then(|suggestion| suggestion.description)
.unwrap_or_default()
.lines()
.count();
let allowed_skips =
description_rows.saturating_sub(self.working_details.description_rows);
if skipped < allowed_skips {
self.skipped_rows = skipped;
} else {
self.skipped_rows = allowed_skips;
}
}
MenuEvent::PreviousPage | MenuEvent::NextPage => {}
}
}
}
/// The buffer gets replaced in the Span location
fn replace_in_buffer(&self, editor: &mut Editor) {
if let Some(Suggestion { value, span, .. }) = self.get_value() {
let start = span.start.min(editor.line_buffer().len());
let end = span.end.min(editor.line_buffer().len());
let replacement = if let Some(example_index) = self.example_index {
self.examples
.get(example_index)
.expect("the example index is always checked")
} else {
&value
};
editor.edit_buffer(
|lb| {
lb.replace_range(start..end, replacement);
let mut offset = lb.insertion_point();
offset += lb.len().saturating_sub(end.saturating_sub(start));
lb.set_insertion_point(offset);
},
UndoBehavior::CreateUndoPoint,
);
}
}
/// Minimum rows that should be displayed by the menu
fn min_rows(&self) -> u16 {
self.get_rows().min(self.min_rows)
}
/// Gets values from filler that will be displayed in the menu
fn get_values(&self) -> &[Suggestion] {
&self.values
}
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
let example_lines = self
.examples
.iter()
.fold(0, |acc, example| example.lines().count() + acc);
self.default_details.selection_rows
+ self.default_details.description_rows as u16
+ example_lines as u16
+ 3
}
fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String {
if self.get_values().is_empty() {
self.no_records_msg(use_ansi_coloring)
} else {
// The skip values represent the number of lines that should be skipped
// while printing the menu
let available_lines = self.default_details.selection_rows;
let skip_values = if self.row_pos >= available_lines {
let skip_lines = self.row_pos.saturating_sub(available_lines) + 1;
(skip_lines * self.get_cols()) as usize
} else {
0
};
// It seems that crossterm prefers to have a complete string ready to be printed
// rather than looping through the values and printing multiple things
// This reduces the flickering when printing the menu
let available_values = (available_lines * self.get_cols()) as usize;
let selection_values: String = self
.get_values()
.iter()
.skip(skip_values)
.take(available_values)
.enumerate()
.map(|(index, suggestion)| {
// Correcting the enumerate index based on the number of skipped values
let index = index + skip_values;
let column = index as u16 % self.get_cols();
let empty_space = self.get_width().saturating_sub(suggestion.value.len());
self.create_entry_string(
suggestion,
index,
column,
empty_space,
use_ansi_coloring,
)
})
.collect();
format!(
"{}{}{}",
selection_values,
self.create_description_string(use_ansi_coloring),
self.create_example_string(use_ansi_coloring)
)
}
}
}

View File

@ -1,8 +1,8 @@
use nu_engine::documentation::get_flags_section; use nu_engine::documentation::get_flags_section;
use nu_protocol::{engine::EngineState, levenshtein_distance}; use nu_protocol::{engine::EngineState, levenshtein_distance};
use nu_utils::IgnoreCaseExt;
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use std::{fmt::Write, sync::Arc}; use std::fmt::Write;
use std::sync::Arc;
pub struct NuHelpCompleter(Arc<EngineState>); pub struct NuHelpCompleter(Arc<EngineState>);
@ -12,51 +12,52 @@ impl NuHelpCompleter {
} }
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> { fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let folded_line = line.to_folded_case(); let full_commands = self.0.get_signatures_with_examples(false);
let mut commands = self //Vec<(Signature, Vec<Example>, bool, bool)> {
.0 let mut commands = full_commands
.get_decls_sorted(false) .iter()
.into_iter() .filter(|(sig, _, _, _, _)| {
.filter_map(|(_, decl_id)| { sig.name.to_lowercase().contains(&line.to_lowercase())
let decl = self.0.get_decl(decl_id); || sig.usage.to_lowercase().contains(&line.to_lowercase())
(decl.name().to_folded_case().contains(&folded_line) || sig
|| decl.usage().to_folded_case().contains(&folded_line) .search_terms
|| decl .iter()
.search_terms() .any(|term| term.to_lowercase().contains(&line.to_lowercase()))
.into_iter() || sig
.any(|term| term.to_folded_case().contains(&folded_line)) .extra_usage
|| decl.extra_usage().to_folded_case().contains(&folded_line)) .to_lowercase()
.then_some(decl) .contains(&line.to_lowercase())
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
commands.sort_by_cached_key(|decl| levenshtein_distance(line, decl.name())); commands.sort_by(|(a, _, _, _, _), (b, _, _, _, _)| {
let a_distance = levenshtein_distance(line, &a.name);
let b_distance = levenshtein_distance(line, &b.name);
a_distance.cmp(&b_distance)
});
commands commands
.into_iter() .into_iter()
.map(|decl| { .map(|(sig, examples, _, _, _)| {
let mut long_desc = String::new(); let mut long_desc = String::new();
let usage = decl.usage(); let usage = &sig.usage;
if !usage.is_empty() { if !usage.is_empty() {
long_desc.push_str(usage); long_desc.push_str(usage);
long_desc.push_str("\r\n\r\n"); long_desc.push_str("\r\n\r\n");
} }
let extra_usage = decl.extra_usage(); let extra_usage = &sig.extra_usage;
if !extra_usage.is_empty() { if !extra_usage.is_empty() {
long_desc.push_str(extra_usage); long_desc.push_str(extra_usage);
long_desc.push_str("\r\n\r\n"); long_desc.push_str("\r\n\r\n");
} }
let sig = decl.signature();
let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature()); let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
if !sig.named.is_empty() { if !sig.named.is_empty() {
long_desc.push_str(&get_flags_section(Some(&*self.0.clone()), &sig, |v| { long_desc.push_str(&get_flags_section(sig))
v.to_parsable_string(", ", &self.0.config)
}))
} }
if !sig.required_positional.is_empty() if !sig.required_positional.is_empty()
@ -68,18 +69,10 @@ impl NuHelpCompleter {
let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc); let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc);
} }
for positional in &sig.optional_positional { for positional in &sig.optional_positional {
let opt_suffix = if let Some(value) = &positional.default_value {
format!(
" (optional, default: {})",
&value.to_parsable_string(", ", &self.0.config),
)
} else {
(" (optional)").to_string()
};
let _ = write!( let _ = write!(
long_desc, long_desc,
" (optional) {}: {}{}\r\n", " (optional) {}: {}\r\n",
positional.name, positional.desc, opt_suffix positional.name, positional.desc
); );
} }
@ -92,20 +85,18 @@ impl NuHelpCompleter {
} }
} }
let extra: Vec<String> = decl let extra: Vec<String> = examples
.examples()
.iter() .iter()
.map(|example| example.example.replace('\n', "\r\n")) .map(|example| example.example.replace('\n', "\r\n"))
.collect(); .collect();
Suggestion { Suggestion {
value: decl.name().into(), value: sig.name.clone(),
description: Some(long_desc), description: Some(long_desc),
style: None,
extra: Some(extra), extra: Some(extra),
span: reedline::Span { span: reedline::Span {
start: pos - line.len(), start: pos,
end: pos, end: pos + line.len(),
}, },
append_whitespace: false, append_whitespace: false,
} }
@ -119,42 +110,3 @@ impl Completer for NuHelpCompleter {
self.completion_helper(line, pos) self.completion_helper(line, pos)
} }
} }
#[cfg(test)]
mod test {
use super::*;
use rstest::rstest;
#[rstest]
#[case("who", 5, 8, &["whoami"])]
#[case("hash", 1, 5, &["hash", "hash md5", "hash sha256"])]
#[case("into f", 0, 6, &["into float", "into filesize"])]
#[case("into nonexistent", 0, 16, &[])]
fn test_help_completer(
#[case] line: &str,
#[case] start: usize,
#[case] end: usize,
#[case] expected: &[&str],
) {
let engine_state =
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
let mut completer = NuHelpCompleter::new(engine_state.into());
let suggestions = completer.complete(line, end);
assert_eq!(
expected.len(),
suggestions.len(),
"expected {:?}, got {:?}",
expected,
suggestions
.iter()
.map(|s| s.value.clone())
.collect::<Vec<_>>()
);
for (exp, actual) in expected.iter().zip(suggestions) {
assert_eq!(exp, &actual.value);
assert_eq!(reedline::Span::new(start, end), actual.span);
}
}
}

View File

@ -1,6 +1,5 @@
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug,
engine::{EngineState, Stack}, engine::{EngineState, Stack},
IntoPipelineData, Span, Value, IntoPipelineData, Span, Value,
}; };
@ -28,7 +27,7 @@ impl NuMenuCompleter {
Self { Self {
block_id, block_id,
span, span,
stack: stack.reset_out_dest().capture(), stack,
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
} }
@ -56,10 +55,17 @@ impl Completer for NuMenuCompleter {
} }
let input = Value::nothing(self.span).into_pipeline_data(); let input = Value::nothing(self.span).into_pipeline_data();
let res = eval_block(
&self.engine_state,
&mut self.stack,
block,
input,
false,
false,
);
let res = eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, block, input); if let Ok(values) = res {
let values = values.into_value(self.span);
if let Ok(values) = res.and_then(|data| data.into_value(self.span)) {
convert_to_suggestions(values, line, pos, self.only_buffer_difference) convert_to_suggestions(values, line, pos, self.only_buffer_difference)
} else { } else {
Vec::new() Vec::new()
@ -74,20 +80,24 @@ fn convert_to_suggestions(
only_buffer_difference: bool, only_buffer_difference: bool,
) -> Vec<Suggestion> { ) -> Vec<Suggestion> {
match value { match value {
Value::Record { val, .. } => { Value::Record { .. } => {
let text = val let text = value
.get("value") .get_data_by_key("value")
.and_then(|val| val.coerce_string().ok()) .and_then(|val| val.as_string().ok())
.unwrap_or_else(|| "No value key".to_string()); .unwrap_or_else(|| "No value key".to_string());
let description = val let description = value
.get("description") .get_data_by_key("description")
.and_then(|val| val.coerce_string().ok()); .and_then(|val| val.as_string().ok());
let span = match val.get("span") { let span = match value.get_data_by_key("span") {
Some(Value::Record { val: span, .. }) => { Some(span @ Value::Record { .. }) => {
let start = span.get("start").and_then(|val| val.as_int().ok()); let start = span
let end = span.get("end").and_then(|val| val.as_int().ok()); .get_data_by_key("start")
.and_then(|val| val.as_integer().ok());
let end = span
.get_data_by_key("end")
.and_then(|val| val.as_integer().ok());
match (start, end) { match (start, end) {
(Some(start), Some(end)) => { (Some(start), Some(end)) => {
let start = start.min(end); let start = start.min(end);
@ -97,13 +107,9 @@ fn convert_to_suggestions(
} }
} }
_ => reedline::Span { _ => reedline::Span {
start: if only_buffer_difference { start: if only_buffer_difference { pos } else { 0 },
pos - line.len()
} else {
0
},
end: if only_buffer_difference { end: if only_buffer_difference {
pos pos + line.len()
} else { } else {
line.len() line.len()
}, },
@ -111,25 +117,21 @@ fn convert_to_suggestions(
} }
} }
_ => reedline::Span { _ => reedline::Span {
start: if only_buffer_difference { start: if only_buffer_difference { pos } else { 0 },
pos - line.len()
} else {
0
},
end: if only_buffer_difference { end: if only_buffer_difference {
pos pos + line.len()
} else { } else {
line.len() line.len()
}, },
}, },
}; };
let extra = match val.get("extra") { let extra = match value.get_data_by_key("extra") {
Some(Value::List { vals, .. }) => { Some(Value::List { vals, .. }) => {
let extra: Vec<String> = vals let extra: Vec<String> = vals
.iter() .into_iter()
.filter_map(|extra| match extra { .filter_map(|extra| match extra {
Value::String { val, .. } => Some(val.clone()), Value::String { val, .. } => Some(val),
_ => None, _ => None,
}) })
.collect(); .collect();
@ -142,7 +144,6 @@ fn convert_to_suggestions(
vec![Suggestion { vec![Suggestion {
value: text, value: text,
description, description,
style: None,
extra, extra,
span, span,
append_whitespace: false, append_whitespace: false,
@ -155,19 +156,10 @@ fn convert_to_suggestions(
_ => vec![Suggestion { _ => vec![Suggestion {
value: format!("Not a record: {value:?}"), value: format!("Not a record: {value:?}"),
description: None, description: None,
style: None,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: if only_buffer_difference { start: 0,
pos - line.len() end: line.len(),
} else {
0
},
end: if only_buffer_difference {
pos
} else {
line.len()
},
}, },
append_whitespace: false, append_whitespace: false,
}], }],

View File

@ -1,5 +1,7 @@
mod description_menu;
mod help_completions; mod help_completions;
mod menu_completions; mod menu_completions;
pub use description_menu::DescriptionMenu;
pub use help_completions::NuHelpCompleter; pub use help_completions::NuHelpCompleter;
pub use menu_completions::NuMenuCompleter; pub use menu_completions::NuMenuCompleter;

View File

@ -1,5 +1,7 @@
use nu_engine::command_prelude::*; use nu_protocol::ast::Call;
use reedline::{Highlighter, StyledText}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Type, Value};
use reedline::Highlighter;
#[derive(Clone)] #[derive(Clone)]
pub struct NuHighlight; pub struct NuHighlight;
@ -26,7 +28,7 @@ impl Command for NuHighlight {
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, _stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
@ -38,17 +40,22 @@ impl Command for NuHighlight {
let highlighter = crate::NuHighlighter { let highlighter = crate::NuHighlighter {
engine_state, engine_state,
stack: std::sync::Arc::new(stack.clone()),
config, config,
}; };
input.map( input.map(
move |x| match x.coerce_into_string() { move |x| match x.as_string() {
Ok(line) => { Ok(line) => {
let highlights = highlighter.highlight(&line, line.len()); let highlights = highlighter.highlight(&line, line.len());
Value::string(highlights.render_simple(), head)
Value::String {
val: highlights.render_simple(),
span: head,
}
} }
Err(err) => Value::error(err, head), Err(err) => Value::Error {
error: Box::new(err),
},
}, },
ctrlc, ctrlc,
) )
@ -62,16 +69,3 @@ impl Command for NuHighlight {
}] }]
} }
} }
/// A highlighter that does nothing
///
/// Used to remove highlighting from a reedline instance
/// (letting NuHighlighter structs be dropped)
#[derive(Default)]
pub struct NoOpHighlighter {}
impl Highlighter for NoOpHighlighter {
fn highlight(&self, _line: &str, _cursor: usize) -> reedline::StyledText {
StyledText::new()
}
}

View File

@ -1,4 +1,10 @@
use nu_engine::command_prelude::*; use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type,
Value,
};
#[derive(Clone)] #[derive(Clone)]
pub struct Print; pub struct Print;
@ -10,11 +16,7 @@ impl Command for Print {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("print") Signature::build("print")
.input_output_types(vec![ .input_output_types(vec![(Type::Nothing, Type::Nothing)])
(Type::Nothing, Type::Nothing),
(Type::Any, Type::Nothing),
])
.allow_variants_without_examples(true)
.rest("rest", SyntaxShape::Any, "the values to print") .rest("rest", SyntaxShape::Any, "the values to print")
.switch( .switch(
"no-newline", "no-newline",
@ -48,8 +50,8 @@ Since this command has no output, there is no point in piping it with other comm
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let args: Vec<Value> = call.rest(engine_state, stack, 0)?; let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
let no_newline = call.has_flag(engine_state, stack, "no-newline")?; let no_newline = call.has_flag("no-newline");
let to_stderr = call.has_flag(engine_state, stack, "stderr")?; let to_stderr = call.has_flag("stderr");
// This will allow for easy printing of pipelines as well // This will allow for easy printing of pipelines as well
if !args.is_empty() { if !args.is_empty() {

View File

@ -1,23 +1,16 @@
use crate::prompt_update::{
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
};
use nu_protocol::{
engine::{EngineState, Stack},
Value,
};
#[cfg(windows)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use reedline::{ use reedline::DefaultPrompt;
DefaultPrompt, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, use {
PromptViMode, reedline::{
Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode,
},
std::borrow::Cow,
}; };
use std::borrow::Cow;
/// Nushell prompt definition /// Nushell prompt definition
#[derive(Clone)] #[derive(Clone)]
pub struct NushellPrompt { pub struct NushellPrompt {
shell_integration_osc133: bool,
shell_integration_osc633: bool,
left_prompt_string: Option<String>, left_prompt_string: Option<String>,
right_prompt_string: Option<String>, right_prompt_string: Option<String>,
default_prompt_indicator: Option<String>, default_prompt_indicator: Option<String>,
@ -25,20 +18,17 @@ pub struct NushellPrompt {
default_vi_normal_prompt_indicator: Option<String>, default_vi_normal_prompt_indicator: Option<String>,
default_multiline_indicator: Option<String>, default_multiline_indicator: Option<String>,
render_right_prompt_on_last_line: bool, render_right_prompt_on_last_line: bool,
engine_state: EngineState, }
stack: Stack,
impl Default for NushellPrompt {
fn default() -> Self {
NushellPrompt::new()
}
} }
impl NushellPrompt { impl NushellPrompt {
pub fn new( pub fn new() -> NushellPrompt {
shell_integration_osc133: bool,
shell_integration_osc633: bool,
engine_state: EngineState,
stack: Stack,
) -> NushellPrompt {
NushellPrompt { NushellPrompt {
shell_integration_osc133,
shell_integration_osc633,
left_prompt_string: None, left_prompt_string: None,
right_prompt_string: None, right_prompt_string: None,
default_prompt_indicator: None, default_prompt_indicator: None,
@ -46,8 +36,6 @@ impl NushellPrompt {
default_vi_normal_prompt_indicator: None, default_vi_normal_prompt_indicator: None,
default_multiline_indicator: None, default_multiline_indicator: None,
render_right_prompt_on_last_line: false, render_right_prompt_on_last_line: false,
engine_state,
stack,
} }
} }
@ -118,28 +106,11 @@ impl Prompt for NushellPrompt {
prompt_string.replace('\n', "\r\n").into() prompt_string.replace('\n', "\r\n").into()
} else { } else {
let default = DefaultPrompt::default(); let default = DefaultPrompt::default();
let prompt = default default
.render_prompt_left() .render_prompt_left()
.to_string() .to_string()
.replace('\n', "\r\n"); .replace('\n', "\r\n")
.into()
if self.shell_integration_osc633 {
if self.stack.get_env_var(&self.engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
// We're in vscode and we have osc633 enabled
format!("{VSCODE_PRE_PROMPT_MARKER}{prompt}{VSCODE_POST_PROMPT_MARKER}").into()
} else if self.shell_integration_osc133 {
// If we're in VSCode but we don't find the env var, but we have osc133 set, then use it
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
} else {
prompt.into()
}
} else if self.shell_integration_osc133 {
format!("{PRE_PROMPT_MARKER}{prompt}{POST_PROMPT_MARKER}").into()
} else {
prompt.into()
}
} }
} }
@ -171,11 +142,11 @@ impl Prompt for NushellPrompt {
PromptEditMode::Vi(vi_mode) => match vi_mode { PromptEditMode::Vi(vi_mode) => match vi_mode {
PromptViMode::Normal => match &self.default_vi_normal_prompt_indicator { PromptViMode::Normal => match &self.default_vi_normal_prompt_indicator {
Some(indicator) => indicator, Some(indicator) => indicator,
None => "> ", None => ": ",
}, },
PromptViMode::Insert => match &self.default_vi_insert_prompt_indicator { PromptViMode::Insert => match &self.default_vi_insert_prompt_indicator {
Some(indicator) => indicator, Some(indicator) => indicator,
None => ": ", None => "> ",
}, },
} }
.into(), .into(),

View File

@ -1,9 +1,10 @@
use crate::util::report_error;
use crate::NushellPrompt; use crate::NushellPrompt;
use log::trace; use log::trace;
use nu_engine::ClosureEvalOnce; use nu_engine::eval_subexpression;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack, StateWorkingSet},
report_error_new, Config, PipelineData, Value, Config, PipelineData, Value,
}; };
use reedline::Prompt; use reedline::Prompt;
@ -14,46 +15,10 @@ pub(crate) const PROMPT_INDICATOR: &str = "PROMPT_INDICATOR";
pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT"; pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT";
pub(crate) const PROMPT_INDICATOR_VI_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL"; pub(crate) const PROMPT_INDICATOR_VI_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL";
pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR"; pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR";
pub(crate) const TRANSIENT_PROMPT_COMMAND: &str = "TRANSIENT_PROMPT_COMMAND";
pub(crate) const TRANSIENT_PROMPT_COMMAND_RIGHT: &str = "TRANSIENT_PROMPT_COMMAND_RIGHT";
pub(crate) const TRANSIENT_PROMPT_INDICATOR: &str = "TRANSIENT_PROMPT_INDICATOR";
pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_INSERT: &str =
"TRANSIENT_PROMPT_INDICATOR_VI_INSERT";
pub(crate) const TRANSIENT_PROMPT_INDICATOR_VI_NORMAL: &str =
"TRANSIENT_PROMPT_INDICATOR_VI_NORMAL";
pub(crate) const TRANSIENT_PROMPT_MULTILINE_INDICATOR: &str =
"TRANSIENT_PROMPT_MULTILINE_INDICATOR";
// Store all these Ansi Escape Markers here so they can be reused easily
// According to Daniel Imms @Tyriar, we need to do these this way: // According to Daniel Imms @Tyriar, we need to do these this way:
// <133 A><prompt><133 B><command><133 C><command output> // <133 A><prompt><133 B><command><133 C><command output>
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\"; const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\"; const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
#[allow(dead_code)]
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
#[allow(dead_code)]
pub(crate) const POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
// OSC633 is the same as OSC133 but specifically for VSCode
pub(crate) const VSCODE_PRE_PROMPT_MARKER: &str = "\x1b]633;A\x1b\\";
pub(crate) const VSCODE_POST_PROMPT_MARKER: &str = "\x1b]633;B\x1b\\";
#[allow(dead_code)]
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
#[allow(dead_code)]
//"\x1b]633;D;{}\x1b\\"
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
#[allow(dead_code)]
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
#[allow(dead_code)]
pub(crate) const VSCODE_COMMANDLINE_MARKER: &str = "\x1b]633;E\x1b\\";
#[allow(dead_code)]
// "\x1b]633;P;Cwd={}\x1b\\"
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
#[allow(dead_code)]
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
fn get_prompt_string( fn get_prompt_string(
prompt: &str, prompt: &str,
@ -64,10 +29,16 @@ fn get_prompt_string(
stack stack
.get_env_var(engine_state, prompt) .get_env_var(engine_state, prompt)
.and_then(|v| match v { .and_then(|v| match v {
Value::Closure { val, .. } => { Value::Closure {
let result = ClosureEvalOnce::new(engine_state, stack, *val) val: block_id,
.run_with_input(PipelineData::Empty); captures,
..
} => {
let block = engine_state.get_block(block_id);
let mut stack = stack.captures_to_stack(&captures);
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
let ret_val =
eval_subexpression(engine_state, &mut stack, block, PipelineData::empty());
trace!( trace!(
"get_prompt_string (block) {}:{}:{}", "get_prompt_string (block) {}:{}:{}",
file!(), file!(),
@ -75,9 +46,28 @@ fn get_prompt_string(
column!() column!()
); );
result ret_val
.map_err(|err| { .map_err(|err| {
report_error_new(engine_state, &err); let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
})
.ok()
}
Value::Block { val: block_id, .. } => {
let block = engine_state.get_block(block_id);
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
let ret_val = eval_subexpression(engine_state, stack, block, PipelineData::empty());
trace!(
"get_prompt_string (block) {}:{}:{}",
file!(),
line!(),
column!()
);
ret_val
.map_err(|err| {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
}) })
.ok() .ok()
} }
@ -101,54 +91,44 @@ fn get_prompt_string(
}) })
} }
pub(crate) fn update_prompt( pub(crate) fn update_prompt<'prompt>(
config: &Config, config: &Config,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &Stack,
nu_prompt: &mut NushellPrompt, nu_prompt: &'prompt mut NushellPrompt,
) { ) -> &'prompt dyn Prompt {
let configured_left_prompt_string = let mut stack = stack.clone();
match get_prompt_string(PROMPT_COMMAND, config, engine_state, stack) {
Some(s) => s, let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, &mut stack);
None => "".to_string(),
};
// Now that we have the prompt string lets ansify it. // Now that we have the prompt string lets ansify it.
// <133 A><prompt><133 B><command><133 C><command output> // <133 A><prompt><133 B><command><133 C><command output>
let left_prompt_string = if config.shell_integration_osc633 { let left_prompt_string = if config.shell_integration {
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) { if let Some(prompt_string) = left_prompt_string {
// We're in vscode and we have osc633 enabled
Some(format!( Some(format!(
"{VSCODE_PRE_PROMPT_MARKER}{configured_left_prompt_string}{VSCODE_POST_PROMPT_MARKER}" "{PRE_PROMPT_MARKER}{prompt_string}{POST_PROMPT_MARKER}"
))
} else if config.shell_integration_osc133 {
// If we're in VSCode but we don't find the env var, but we have osc133 set, then use it
Some(format!(
"{PRE_PROMPT_MARKER}{configured_left_prompt_string}{POST_PROMPT_MARKER}"
)) ))
} else { } else {
configured_left_prompt_string.into() left_prompt_string
} }
} else if config.shell_integration_osc133 {
Some(format!(
"{PRE_PROMPT_MARKER}{configured_left_prompt_string}{POST_PROMPT_MARKER}"
))
} else { } else {
configured_left_prompt_string.into() left_prompt_string
}; };
let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack); let right_prompt_string =
get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, &mut stack);
let prompt_indicator_string = get_prompt_string(PROMPT_INDICATOR, config, engine_state, stack); let prompt_indicator_string =
get_prompt_string(PROMPT_INDICATOR, config, engine_state, &mut stack);
let prompt_multiline_string = let prompt_multiline_string =
get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, stack); get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, &mut stack);
let prompt_vi_insert_string = let prompt_vi_insert_string =
get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, stack); get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, &mut stack);
let prompt_vi_normal_string = let prompt_vi_normal_string =
get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, stack); get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, &mut stack);
// apply the other indicators // apply the other indicators
nu_prompt.update_all_prompt_strings( nu_prompt.update_all_prompt_strings(
@ -159,55 +139,9 @@ pub(crate) fn update_prompt(
(prompt_vi_insert_string, prompt_vi_normal_string), (prompt_vi_insert_string, prompt_vi_normal_string),
config.render_right_prompt_on_last_line, config.render_right_prompt_on_last_line,
); );
let ret_val = nu_prompt as &dyn Prompt;
trace!("update_prompt {}:{}:{}", file!(), line!(), column!()); trace!("update_prompt {}:{}:{}", file!(), line!(), column!());
}
ret_val
/// Construct the transient prompt based on the normal nu_prompt
pub(crate) fn make_transient_prompt(
config: &Config,
engine_state: &EngineState,
stack: &mut Stack,
nu_prompt: &NushellPrompt,
) -> Box<dyn Prompt> {
let mut nu_prompt = nu_prompt.clone();
if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND, config, engine_state, stack) {
nu_prompt.update_prompt_left(Some(s))
}
if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_COMMAND_RIGHT, config, engine_state, stack)
{
nu_prompt.update_prompt_right(Some(s), config.render_right_prompt_on_last_line)
}
if let Some(s) = get_prompt_string(TRANSIENT_PROMPT_INDICATOR, config, engine_state, stack) {
nu_prompt.update_prompt_indicator(Some(s))
}
if let Some(s) = get_prompt_string(
TRANSIENT_PROMPT_INDICATOR_VI_INSERT,
config,
engine_state,
stack,
) {
nu_prompt.update_prompt_vi_insert(Some(s))
}
if let Some(s) = get_prompt_string(
TRANSIENT_PROMPT_INDICATOR_VI_NORMAL,
config,
engine_state,
stack,
) {
nu_prompt.update_prompt_vi_normal(Some(s))
}
if let Some(s) = get_prompt_string(
TRANSIENT_PROMPT_MULTILINE_INDICATOR,
config,
engine_state,
stack,
) {
nu_prompt.update_prompt_multiline(Some(s))
}
Box::new(nu_prompt)
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,15 @@
use log::trace; use log::trace;
use nu_ansi_term::Style; use nu_ansi_term::Style;
use nu_color_config::{get_matching_brackets_style, get_shape_color}; use nu_color_config::{get_matching_brackets_style, get_shape_color};
use nu_engine::env;
use nu_parser::{flatten_block, parse, FlatShape}; use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{ use nu_protocol::ast::{Argument, Block, Expr, Expression, PipelineElement};
ast::{Block, Expr, Expression, PipelineRedirection, RecordItem}, use nu_protocol::engine::{EngineState, StateWorkingSet};
engine::{EngineState, Stack, StateWorkingSet}, use nu_protocol::{Config, Span};
Config, Span,
};
use reedline::{Highlighter, StyledText}; use reedline::{Highlighter, StyledText};
use std::sync::Arc; use std::sync::Arc;
pub struct NuHighlighter { pub struct NuHighlighter {
pub engine_state: Arc<EngineState>, pub engine_state: Arc<EngineState>,
pub stack: Arc<Stack>,
pub config: Config, pub config: Config,
} }
@ -21,38 +17,13 @@ impl Highlighter for NuHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> StyledText { fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
trace!("highlighting: {}", line); trace!("highlighting: {}", line);
let highlight_resolved_externals =
self.engine_state.get_config().highlight_resolved_externals;
let mut working_set = StateWorkingSet::new(&self.engine_state); let mut working_set = StateWorkingSet::new(&self.engine_state);
let block = parse(&mut working_set, None, line.as_bytes(), false); let block = {
let (block, _) = parse(&mut working_set, None, line.as_bytes(), false, &[]);
block
};
let (shapes, global_span_offset) = { let (shapes, global_span_offset) = {
let mut shapes = flatten_block(&working_set, &block); let shapes = flatten_block(&working_set, &block);
// Highlighting externals has a config point because of concerns that using which to resolve
// externals may slow down things too much.
if highlight_resolved_externals {
for (span, shape) in shapes.iter_mut() {
if *shape == FlatShape::External {
let str_contents =
working_set.get_span_contents(Span::new(span.start, span.end));
let str_word = String::from_utf8_lossy(str_contents).to_string();
let paths = env::path_str(&self.engine_state, &self.stack, *span).ok();
#[allow(deprecated)]
let res = if let Ok(cwd) =
env::current_dir_str(&self.engine_state, &self.stack)
{
which::which_in(str_word, paths.as_ref(), cwd).ok()
} else {
which::which_in_global(str_word, paths.as_ref())
.ok()
.and_then(|mut i| i.next())
};
if res.is_some() {
*shape = FlatShape::ExternalResolved;
}
}
}
}
(shapes, self.engine_state.next_span_start()) (shapes, self.engine_state.next_span_start())
}; };
@ -87,68 +58,74 @@ impl Highlighter for NuHighlighter {
[(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)]
.to_string(); .to_string();
let mut add_colored_token = |shape: &FlatShape, text: String| { macro_rules! add_colored_token_with_bracket_highlight {
output.push((get_shape_color(shape.as_str(), &self.config), text)); ($shape:expr, $span:expr, $text:expr) => {{
};
match shape.1 {
FlatShape::Garbage => add_colored_token(&shape.1, next_token),
FlatShape::Nothing => add_colored_token(&shape.1, next_token),
FlatShape::Binary => add_colored_token(&shape.1, next_token),
FlatShape::Bool => add_colored_token(&shape.1, next_token),
FlatShape::Int => add_colored_token(&shape.1, next_token),
FlatShape::Float => add_colored_token(&shape.1, next_token),
FlatShape::Range => add_colored_token(&shape.1, next_token),
FlatShape::InternalCall(_) => add_colored_token(&shape.1, next_token),
FlatShape::External => add_colored_token(&shape.1, next_token),
FlatShape::ExternalArg => add_colored_token(&shape.1, next_token),
FlatShape::ExternalResolved => add_colored_token(&shape.1, next_token),
FlatShape::Keyword => add_colored_token(&shape.1, next_token),
FlatShape::Literal => add_colored_token(&shape.1, next_token),
FlatShape::Operator => add_colored_token(&shape.1, next_token),
FlatShape::Signature => add_colored_token(&shape.1, next_token),
FlatShape::String => add_colored_token(&shape.1, next_token),
FlatShape::RawString => add_colored_token(&shape.1, next_token),
FlatShape::StringInterpolation => add_colored_token(&shape.1, next_token),
FlatShape::DateTime => add_colored_token(&shape.1, next_token),
FlatShape::List
| FlatShape::Table
| FlatShape::Record
| FlatShape::Block
| FlatShape::Closure => {
let span = shape.0;
let shape = &shape.1;
let spans = split_span_by_highlight_positions( let spans = split_span_by_highlight_positions(
line, line,
span, &$span,
&matching_brackets_pos, &matching_brackets_pos,
global_span_offset, global_span_offset,
); );
for (part, highlight) in spans { spans.iter().for_each(|(part, highlight)| {
let start = part.start - span.start; let start = part.start - $span.start;
let end = part.end - span.start; let end = part.end - $span.start;
let text = next_token[start..end].to_string(); let text = (&next_token[start..end]).to_string();
let mut style = get_shape_color(shape.as_str(), &self.config); let mut style = get_shape_color($shape.to_string(), &self.config);
if highlight { if *highlight {
style = get_matching_brackets_style(style, &self.config); style = get_matching_brackets_style(style, &self.config);
} }
output.push((style, text)); output.push((style, text));
} });
}};
}
macro_rules! add_colored_token {
($shape:expr, $text:expr) => {
output.push((get_shape_color($shape.to_string(), &self.config), $text))
};
}
match shape.1 {
FlatShape::Garbage => add_colored_token!(shape.1, next_token),
FlatShape::Nothing => add_colored_token!(shape.1, next_token),
FlatShape::Binary => add_colored_token!(shape.1, next_token),
FlatShape::Bool => add_colored_token!(shape.1, next_token),
FlatShape::Int => add_colored_token!(shape.1, next_token),
FlatShape::Float => add_colored_token!(shape.1, next_token),
FlatShape::Range => add_colored_token!(shape.1, next_token),
FlatShape::InternalCall => add_colored_token!(shape.1, next_token),
FlatShape::External => add_colored_token!(shape.1, next_token),
FlatShape::ExternalArg => add_colored_token!(shape.1, next_token),
FlatShape::Literal => add_colored_token!(shape.1, next_token),
FlatShape::Operator => add_colored_token!(shape.1, next_token),
FlatShape::Signature => add_colored_token!(shape.1, next_token),
FlatShape::String => add_colored_token!(shape.1, next_token),
FlatShape::StringInterpolation => add_colored_token!(shape.1, next_token),
FlatShape::DateTime => add_colored_token!(shape.1, next_token),
FlatShape::List => {
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
}
FlatShape::Table => {
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
}
FlatShape::Record => {
add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
} }
FlatShape::Filepath => add_colored_token(&shape.1, next_token), FlatShape::Block => {
FlatShape::Directory => add_colored_token(&shape.1, next_token), add_colored_token_with_bracket_highlight!(shape.1, shape.0, next_token)
FlatShape::GlobPattern => add_colored_token(&shape.1, next_token),
FlatShape::Variable(_) | FlatShape::VarDecl(_) => {
add_colored_token(&shape.1, next_token)
} }
FlatShape::Flag => add_colored_token(&shape.1, next_token),
FlatShape::Pipe => add_colored_token(&shape.1, next_token), FlatShape::Filepath => add_colored_token!(shape.1, next_token),
FlatShape::And => add_colored_token(&shape.1, next_token), FlatShape::Directory => add_colored_token!(shape.1, next_token),
FlatShape::Or => add_colored_token(&shape.1, next_token), FlatShape::GlobPattern => add_colored_token!(shape.1, next_token),
FlatShape::Redirection => add_colored_token(&shape.1, next_token), FlatShape::Variable => add_colored_token!(shape.1, next_token),
FlatShape::Custom(..) => add_colored_token(&shape.1, next_token), FlatShape::Flag => add_colored_token!(shape.1, next_token),
FlatShape::MatchPattern => add_colored_token(&shape.1, next_token), FlatShape::Pipe => add_colored_token!(shape.1, next_token),
FlatShape::And => add_colored_token!(shape.1, next_token),
FlatShape::Or => add_colored_token!(shape.1, next_token),
FlatShape::Redirection => add_colored_token!(shape.1, next_token),
FlatShape::Custom(..) => add_colored_token!(shape.1, next_token),
} }
last_seen_span = shape.0.end; last_seen_span = shape.0.end;
} }
@ -164,8 +141,8 @@ impl Highlighter for NuHighlighter {
fn split_span_by_highlight_positions( fn split_span_by_highlight_positions(
line: &str, line: &str,
span: Span, span: &Span,
highlight_positions: &[usize], highlight_positions: &Vec<usize>,
global_span_offset: usize, global_span_offset: usize,
) -> Vec<(Span, bool)> { ) -> Vec<(Span, bool)> {
let mut start = span.start; let mut start = span.start;
@ -253,38 +230,23 @@ fn find_matching_block_end_in_block(
) -> Option<usize> { ) -> Option<usize> {
for p in &block.pipelines { for p in &block.pipelines {
for e in &p.elements { for e in &p.elements {
if e.expr.span.contains(global_cursor_offset) { match e {
if let Some(pos) = find_matching_block_end_in_expr( PipelineElement::Expression(_, e)
line, | PipelineElement::Redirection(_, _, e)
working_set, | PipelineElement::And(_, e)
&e.expr, | PipelineElement::Or(_, e)
global_span_offset, | PipelineElement::SeparateRedirection { out: (_, e), .. } => {
global_cursor_offset, if e.span.contains(global_cursor_offset) {
) { if let Some(pos) = find_matching_block_end_in_expr(
return Some(pos); line,
} working_set,
} e,
global_span_offset,
if let Some(redirection) = e.redirection.as_ref() { global_cursor_offset,
match redirection { ) {
PipelineRedirection::Single { target, .. }
| PipelineRedirection::Separate { out: target, .. }
| PipelineRedirection::Separate { err: target, .. }
if target.span().contains(global_cursor_offset) =>
{
if let Some(pos) = target.expr().and_then(|expr| {
find_matching_block_end_in_expr(
line,
working_set,
expr,
global_span_offset,
global_cursor_offset,
)
}) {
return Some(pos); return Some(pos);
} }
} }
_ => {}
} }
} }
} }
@ -299,6 +261,20 @@ fn find_matching_block_end_in_expr(
global_span_offset: usize, global_span_offset: usize,
global_cursor_offset: usize, global_cursor_offset: usize,
) -> Option<usize> { ) -> Option<usize> {
macro_rules! find_in_expr_or_continue {
($inner_expr:ident) => {
if let Some(pos) = find_matching_block_end_in_expr(
line,
working_set,
$inner_expr,
global_span_offset,
global_cursor_offset,
) {
return Some(pos);
}
};
}
if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset
{ {
let expr_first = expression.span.start; let expr_first = expression.span.start;
@ -324,20 +300,18 @@ fn find_matching_block_end_in_expr(
Expr::Keyword(..) => None, Expr::Keyword(..) => None,
Expr::ValueWithUnit(..) => None, Expr::ValueWithUnit(..) => None,
Expr::DateTime(_) => None, Expr::DateTime(_) => None,
Expr::Filepath(_, _) => None, Expr::Filepath(_) => None,
Expr::Directory(_, _) => None, Expr::Directory(_) => None,
Expr::GlobPattern(_, _) => None, Expr::GlobPattern(_) => None,
Expr::String(_) => None, Expr::String(_) => None,
Expr::RawString(_) => None,
Expr::CellPath(_) => None, Expr::CellPath(_) => None,
Expr::ImportPattern(_) => None, Expr::ImportPattern(_) => None,
Expr::Overlay(_) => None, Expr::Overlay(_) => None,
Expr::Signature(_) => None, Expr::Signature(_) => None,
Expr::MatchBlock(_) => None,
Expr::Nothing => None, Expr::Nothing => None,
Expr::Garbage => None, Expr::Garbage => None,
Expr::Table(table) => { Expr::Table(hdr, rows) => {
if expr_last == global_cursor_offset { if expr_last == global_cursor_offset {
// cursor is at table end // cursor is at table end
Some(expr_first) Some(expr_first)
@ -346,19 +320,15 @@ fn find_matching_block_end_in_expr(
Some(expr_last) Some(expr_last)
} else { } else {
// cursor is inside table // cursor is inside table
table for inner_expr in hdr {
.columns find_in_expr_or_continue!(inner_expr);
.iter() }
.chain(table.rows.iter().flat_map(AsRef::as_ref)) for row in rows {
.find_map(|expr| { for inner_expr in row {
find_matching_block_end_in_expr( find_in_expr_or_continue!(inner_expr);
line, }
working_set, }
expr, None
global_span_offset,
global_cursor_offset,
)
})
} }
} }
@ -371,45 +341,28 @@ fn find_matching_block_end_in_expr(
Some(expr_last) Some(expr_last)
} else { } else {
// cursor is inside record // cursor is inside record
exprs.iter().find_map(|expr| match expr { for (k, v) in exprs {
RecordItem::Pair(k, v) => find_matching_block_end_in_expr( find_in_expr_or_continue!(k);
line, find_in_expr_or_continue!(v);
working_set, }
k, None
global_span_offset,
global_cursor_offset,
)
.or_else(|| {
find_matching_block_end_in_expr(
line,
working_set,
v,
global_span_offset,
global_cursor_offset,
)
}),
RecordItem::Spread(_, record) => find_matching_block_end_in_expr(
line,
working_set,
record,
global_span_offset,
global_cursor_offset,
),
})
} }
} }
Expr::Call(call) => call.arguments.iter().find_map(|arg| { Expr::Call(call) => {
arg.expr().and_then(|expr| { for arg in &call.arguments {
find_matching_block_end_in_expr( let opt_expr = match arg {
line, Argument::Named((_, _, opt_expr)) => opt_expr.as_ref(),
working_set, Argument::Positional(inner_expr) => Some(inner_expr),
expr, Argument::Unknown(inner_expr) => Some(inner_expr),
global_span_offset, };
global_cursor_offset,
) if let Some(inner_expr) = opt_expr {
}) find_in_expr_or_continue!(inner_expr);
}), }
}
None
}
Expr::FullCellPath(b) => find_matching_block_end_in_expr( Expr::FullCellPath(b) => find_matching_block_end_in_expr(
line, line,
@ -419,15 +372,12 @@ fn find_matching_block_end_in_expr(
global_cursor_offset, global_cursor_offset,
), ),
Expr::BinaryOp(lhs, op, rhs) => [lhs, op, rhs].into_iter().find_map(|expr| { Expr::BinaryOp(lhs, op, rhs) => {
find_matching_block_end_in_expr( find_in_expr_or_continue!(lhs);
line, find_in_expr_or_continue!(op);
working_set, find_in_expr_or_continue!(rhs);
expr, None
global_span_offset, }
global_cursor_offset,
)
}),
Expr::Block(block_id) Expr::Block(block_id)
| Expr::Closure(block_id) | Expr::Closure(block_id)
@ -452,17 +402,14 @@ fn find_matching_block_end_in_expr(
} }
} }
Expr::StringInterpolation(exprs) => exprs.iter().find_map(|expr| { Expr::StringInterpolation(inner_expr) => {
find_matching_block_end_in_expr( for inner_expr in inner_expr {
line, find_in_expr_or_continue!(inner_expr);
working_set, }
expr, None
global_span_offset, }
global_cursor_offset,
)
}),
Expr::List(list) => { Expr::List(inner_expr) => {
if expr_last == global_cursor_offset { if expr_last == global_cursor_offset {
// cursor is at list end // cursor is at list end
Some(expr_first) Some(expr_first)
@ -470,15 +417,11 @@ fn find_matching_block_end_in_expr(
// cursor is at list start // cursor is at list start
Some(expr_last) Some(expr_last)
} else { } else {
list.iter().find_map(|item| { // cursor is inside list
find_matching_block_end_in_expr( for inner_expr in inner_expr {
line, find_in_expr_or_continue!(inner_expr);
working_set, }
item.expr(), None
global_span_offset,
global_cursor_offset,
)
})
} }
} }
}; };

View File

@ -1,15 +1,16 @@
use nu_cmd_base::hook::eval_hook; use crate::repl::eval_hook;
use nu_engine::{eval_block, eval_block_with_early_return}; use nu_engine::{eval_block, eval_block_with_early_return};
use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents}; use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
use nu_protocol::engine::StateWorkingSet;
use nu_protocol::CliError;
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug, engine::{EngineState, Stack},
engine::{EngineState, Stack, StateWorkingSet}, print_if_stream, PipelineData, ShellError, Span, Value,
report_error, report_error_new, PipelineData, ShellError, Span, Value,
}; };
#[cfg(windows)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use nu_utils::utils::perf; use nu_utils::utils::perf;
use std::path::Path; use std::path::{Path, PathBuf};
// This will collect environment variables from std::env and adds them to a stack. // This will collect environment variables from std::env and adds them to a stack.
// //
@ -39,15 +40,16 @@ fn gather_env_vars(
init_cwd: &Path, init_cwd: &Path,
) { ) {
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) { fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
report_error_new( let working_set = StateWorkingSet::new(engine_state);
engine_state, report_error(
&ShellError::GenericError { &working_set,
error: format!("Environment variable was not captured: {env_str}"), &ShellError::GenericError(
msg: "".into(), format!("Environment variable was not captured: {env_str}"),
span: None, "".to_string(),
help: Some(msg.into()), None,
inner: vec![], Some(msg.into()),
}, Vec::new(),
),
); );
} }
@ -70,17 +72,18 @@ fn gather_env_vars(
} }
None => { None => {
// Could not capture current working directory // Could not capture current working directory
report_error_new( let working_set = StateWorkingSet::new(engine_state);
engine_state, report_error(
&ShellError::GenericError { &working_set,
error: "Current directory is not a valid utf-8 path".into(), &ShellError::GenericError(
msg: "".into(), "Current directory is not a valid utf-8 path".to_string(),
span: None, "".to_string(),
help: Some(format!( None,
Some(format!(
"Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path" "Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path"
)), )),
inner: vec![], Vec::new(),
}, ),
); );
} }
} }
@ -90,8 +93,8 @@ fn gather_env_vars(
let span_offset = engine_state.next_span_start(); let span_offset = engine_state.next_span_start();
engine_state.add_file( engine_state.add_file(
"Host Environment Variables".into(), "Host Environment Variables".to_string(),
fake_env_file.as_bytes().into(), fake_env_file.as_bytes().to_vec(),
); );
let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true); let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
@ -102,16 +105,15 @@ fn gather_env_vars(
span: full_span, span: full_span,
} = token } = token
{ {
let contents = engine_state.get_span_contents(full_span); let contents = engine_state.get_span_contents(&full_span);
let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true); let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
let name = if let Some(Token { let name = if let Some(Token {
contents: TokenContents::Item, contents: TokenContents::Item,
span, span,
}) = parts.first() }) = parts.get(0)
{ {
let mut working_set = StateWorkingSet::new(engine_state); let bytes = engine_state.get_span_contents(span);
let bytes = working_set.get_span_contents(*span);
if bytes.len() < 2 { if bytes.len() < 2 {
report_capture_error( report_capture_error(
@ -123,12 +125,9 @@ fn gather_env_vars(
continue; continue;
} }
let (bytes, err) = unescape_unquote_string(bytes, *span); let (bytes, parse_error) = unescape_unquote_string(bytes, *span);
if let Some(err) = err {
working_set.error(err);
}
if working_set.parse_errors.first().is_some() { if parse_error.is_some() {
report_capture_error( report_capture_error(
engine_state, engine_state,
&String::from_utf8_lossy(contents), &String::from_utf8_lossy(contents),
@ -154,8 +153,7 @@ fn gather_env_vars(
span, span,
}) = parts.get(2) }) = parts.get(2)
{ {
let mut working_set = StateWorkingSet::new(engine_state); let bytes = engine_state.get_span_contents(span);
let bytes = working_set.get_span_contents(*span);
if bytes.len() < 2 { if bytes.len() < 2 {
report_capture_error( report_capture_error(
@ -167,12 +165,9 @@ fn gather_env_vars(
continue; continue;
} }
let (bytes, err) = unescape_unquote_string(bytes, *span); let (bytes, parse_error) = unescape_unquote_string(bytes, *span);
if let Some(err) = err {
working_set.error(err);
}
if working_set.parse_errors.first().is_some() { if parse_error.is_some() {
report_capture_error( report_capture_error(
engine_state, engine_state,
&String::from_utf8_lossy(contents), &String::from_utf8_lossy(contents),
@ -182,7 +177,10 @@ fn gather_env_vars(
continue; continue;
} }
Value::string(bytes, *span) Value::String {
val: bytes,
span: *span,
}
} else { } else {
report_capture_error( report_capture_error(
engine_state, engine_state,
@ -206,28 +204,93 @@ pub fn eval_source(
fname: &str, fname: &str,
input: PipelineData, input: PipelineData,
allow_return: bool, allow_return: bool,
) -> i32 { ) -> bool {
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) { let (block, delta) = {
Ok(code) => code.unwrap_or(0), let mut working_set = StateWorkingSet::new(engine_state);
Err(err) => { let (output, err) = parse(
report_error_new(engine_state, &err); &mut working_set,
1 Some(fname), // format!("entry #{}", entry_num)
source,
false,
&[],
);
if let Some(err) = err {
set_last_exit_code(stack, 1);
report_error(&working_set, &err);
return false;
} }
(output, working_set.render())
}; };
stack.add_env_var( if let Err(err) = engine_state.merge_delta(delta) {
"LAST_EXIT_CODE".to_string(), set_last_exit_code(stack, 1);
Value::int(exit_code.into(), Span::unknown()), report_error_new(engine_state, &err);
); return false;
// reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)]
{
let _ = enable_vt_processing();
} }
let b = if allow_return {
eval_block_with_early_return(engine_state, stack, &block, input, false, false)
} else {
eval_block(engine_state, stack, &block, input, false, false)
};
match b {
Ok(pipeline_data) => {
let config = engine_state.get_config();
let result;
if let PipelineData::ExternalStream {
stdout: stream,
stderr: stderr_stream,
exit_code,
..
} = pipeline_data
{
result = print_if_stream(stream, stderr_stream, false, exit_code);
} else if let Some(hook) = config.hooks.display_output.clone() {
match eval_hook(engine_state, stack, Some(pipeline_data), vec![], &hook) {
Err(err) => {
result = Err(err);
}
Ok(val) => {
result = val.print(engine_state, stack, false, false);
}
}
} else {
result = pipeline_data.print(engine_state, stack, true, false);
}
match result {
Err(err) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
return false;
}
Ok(exit_code) => {
set_last_exit_code(stack, exit_code);
}
}
// reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)]
{
let _ = enable_vt_processing();
}
}
Err(err) => {
set_last_exit_code(stack, 1);
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &err);
return false;
}
}
perf( perf(
&format!("eval_source {}", &fname), &format!("eval_source {}", &fname),
start_time, start_time,
@ -237,64 +300,51 @@ pub fn eval_source(
engine_state.get_config().use_ansi_coloring, engine_state.get_config().use_ansi_coloring,
); );
exit_code true
} }
fn evaluate_source( fn set_last_exit_code(stack: &mut Stack, exit_code: i64) {
engine_state: &mut EngineState, stack.add_env_var(
stack: &mut Stack, "LAST_EXIT_CODE".to_string(),
source: &[u8], Value::int(exit_code, Span::unknown()),
fname: &str, );
input: PipelineData, }
allow_return: bool,
) -> Result<Option<i32>, ShellError> {
let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state);
let output = parse(
&mut working_set,
Some(fname), // format!("entry #{}", entry_num)
source,
false,
);
if let Some(warning) = working_set.parse_warnings.first() {
report_error(&working_set, warning);
}
if let Some(err) = working_set.parse_errors.first() { pub fn report_error(
report_error(&working_set, err); working_set: &StateWorkingSet,
return Ok(Some(1)); error: &(dyn miette::Diagnostic + Send + Sync + 'static),
} ) {
eprintln!("Error: {:?}", CliError(error, working_set));
// reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)]
{
let _ = nu_utils::enable_vt_processing();
}
}
(output, working_set.render()) pub fn report_error_new(
}; engine_state: &EngineState,
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
) {
let working_set = StateWorkingSet::new(engine_state);
engine_state.merge_delta(delta)?; report_error(&working_set, error);
}
let pipeline = if allow_return { pub fn get_init_cwd() -> PathBuf {
eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input) std::env::current_dir().unwrap_or_else(|_| {
} else { std::env::var("PWD")
eval_block::<WithoutDebug>(engine_state, stack, &block, input) .map(Into::into)
}?; .unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default())
})
}
let status = if let PipelineData::ByteStream(..) = pipeline { pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf {
pipeline.print(engine_state, stack, false, false)? nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| {
} else { let working_set = StateWorkingSet::new(engine_state);
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() { report_error(&working_set, &e);
let pipeline = eval_hook( get_init_cwd()
engine_state, })
stack,
Some(pipeline),
vec![],
&hook,
"display_output",
)?;
pipeline.print(engine_state, stack, false, false)
} else {
pipeline.print(engine_state, stack, true, false)
}?
};
Ok(status.map(|status| status.code()))
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,8 +1,5 @@
use nu_parser::parse; use nu_parser::{parse, ParseError};
use nu_protocol::{ use nu_protocol::engine::{EngineState, StateWorkingSet};
engine::{EngineState, StateWorkingSet},
ParseError,
};
use reedline::{ValidationResult, Validator}; use reedline::{ValidationResult, Validator};
use std::sync::Arc; use std::sync::Arc;
@ -13,12 +10,9 @@ pub struct NuValidator {
impl Validator for NuValidator { impl Validator for NuValidator {
fn validate(&self, line: &str) -> ValidationResult { fn validate(&self, line: &str) -> ValidationResult {
let mut working_set = StateWorkingSet::new(&self.engine_state); let mut working_set = StateWorkingSet::new(&self.engine_state);
parse(&mut working_set, None, line.as_bytes(), false); let (_, err) = parse(&mut working_set, None, line.as_bytes(), false, &[]);
if matches!( if matches!(err, Some(ParseError::UnexpectedEof(..))) {
working_set.parse_errors.first(),
Some(ParseError::UnexpectedEof(..))
) {
ValidationResult::Incomplete ValidationResult::Incomplete
} else { } else {
ValidationResult::Complete ValidationResult::Complete

View File

@ -1 +0,0 @@
mod nu_highlight;

View File

@ -1,7 +0,0 @@
use nu_test_support::nu;
#[test]
fn nu_highlight_not_expr() {
let actual = nu!("'not false' | nu-highlight | ansi strip");
assert_eq!(actual.out, "not false");
}

View File

@ -1,19 +1,11 @@
pub mod support; pub mod support;
use nu_cli::NuCompleter; use nu_cli::NuCompleter;
use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData}; use nu_protocol::engine::StateWorkingSet;
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
use rstest::{fixture, rstest}; use rstest::{fixture, rstest};
use std::{ use support::{completions_helpers::new_quote_engine, file, folder, match_suggestions, new_engine};
path::{PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use support::{
completions_helpers::{new_partial_engine, new_quote_engine},
file, folder, match_suggestions, new_engine,
};
#[fixture] #[fixture]
fn completer() -> NuCompleter { fn completer() -> NuCompleter {
@ -25,7 +17,7 @@ fn completer() -> NuCompleter {
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
// Instantiate a new completer // Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack)) NuCompleter::new(std::sync::Arc::new(engine), stack)
} }
#[fixture] #[fixture]
@ -39,7 +31,7 @@ fn completer_strings() -> NuCompleter {
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
// Instantiate a new completer // Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack)) NuCompleter::new(std::sync::Arc::new(engine), stack)
} }
#[fixture] #[fixture]
@ -59,42 +51,19 @@ fn extern_completer() -> NuCompleter {
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
// Instantiate a new completer // Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack)) NuCompleter::new(std::sync::Arc::new(engine), stack)
}
#[fixture]
fn custom_completer() -> NuCompleter {
// Create a new engine
let (dir, _, mut engine, mut stack) = new_engine();
// Add record value as example
let record = r#"
let external_completer = {|spans|
$spans
}
$env.config.completions.external = {
enable: true
max_results: 100
completer: $external_completer
}
"#;
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
// Instantiate a new completer
NuCompleter::new(Arc::new(engine), Arc::new(stack))
} }
#[test] #[test]
fn variables_dollar_sign_with_varialblecompletion() { fn variables_dollar_sign_with_varialblecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "$ "; let target_dir = "$ ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
assert_eq!(8, suggestions.len()); assert_eq!(7, suggestions.len());
} }
#[rstest] #[rstest]
@ -141,75 +110,56 @@ fn dotnu_completions() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test source completion // Test source completion
let completion_str = "source-env ".to_string(); let completion_str = "source-env ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len()); let suggestions = completer.complete(&completion_str, completion_str.len());
assert_eq!(2, suggestions.len()); assert_eq!(1, suggestions.len());
assert_eq!("custom_completion.nu", suggestions.first().unwrap().value); assert_eq!("custom_completion.nu", suggestions.get(0).unwrap().value);
#[cfg(windows)]
assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value);
#[cfg(not(windows))]
assert_eq!("directory_completion/", suggestions.get(1).unwrap().value);
// Test use completion // Test use completion
let completion_str = "use ".to_string(); let completion_str = "use ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len()); let suggestions = completer.complete(&completion_str, completion_str.len());
assert_eq!(2, suggestions.len()); assert_eq!(1, suggestions.len());
assert_eq!("custom_completion.nu", suggestions.first().unwrap().value); assert_eq!("custom_completion.nu", suggestions.get(0).unwrap().value);
#[cfg(windows)]
assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value);
#[cfg(not(windows))]
assert_eq!("directory_completion/", suggestions.get(1).unwrap().value);
// Test overlay use completion
let completion_str = "overlay use ".to_string();
let suggestions = completer.complete(&completion_str, completion_str.len());
assert_eq!(2, suggestions.len());
assert_eq!("custom_completion.nu", suggestions.first().unwrap().value);
#[cfg(windows)]
assert_eq!("directory_completion\\", suggestions.get(1).unwrap().value);
#[cfg(not(windows))]
assert_eq!("directory_completion/", suggestions.get(1).unwrap().value);
} }
#[test] #[test]
#[ignore] #[ignore]
fn external_completer_trailing_space() { fn external_completer_trailing_space() {
// https://github.com/nushell/nushell/issues/6378 // https://github.com/nushell/nushell/issues/6378
let block = "{|spans| $spans}"; let block = "let external_completer = {|spans| $spans}";
let input = "gh alias ".to_string(); let input = "gh alias ".to_string();
let suggestions = run_external_completion(block, &input); let suggestions = run_external_completion(block, &input);
assert_eq!(3, suggestions.len()); assert_eq!(3, suggestions.len());
assert_eq!("gh", suggestions.first().unwrap().value); assert_eq!("gh", suggestions.get(0).unwrap().value);
assert_eq!("alias", suggestions.get(1).unwrap().value); assert_eq!("alias", suggestions.get(1).unwrap().value);
assert_eq!("", suggestions.get(2).unwrap().value); assert_eq!("", suggestions.get(2).unwrap().value);
} }
#[test] #[test]
fn external_completer_no_trailing_space() { fn external_completer_no_trailing_space() {
let block = "{|spans| $spans}"; let block = "let external_completer = {|spans| $spans}";
let input = "gh alias".to_string(); let input = "gh alias".to_string();
let suggestions = run_external_completion(block, &input); let suggestions = run_external_completion(block, &input);
assert_eq!(2, suggestions.len()); assert_eq!(2, suggestions.len());
assert_eq!("gh", suggestions.first().unwrap().value); assert_eq!("gh", suggestions.get(0).unwrap().value);
assert_eq!("alias", suggestions.get(1).unwrap().value); assert_eq!("alias", suggestions.get(1).unwrap().value);
} }
#[test] #[test]
fn external_completer_pass_flags() { fn external_completer_pass_flags() {
let block = "{|spans| $spans}"; let block = "let external_completer = {|spans| $spans}";
let input = "gh api --".to_string(); let input = "gh api --".to_string();
let suggestions = run_external_completion(block, &input); let suggestions = run_external_completion(block, &input);
assert_eq!(3, suggestions.len()); assert_eq!(3, suggestions.len());
assert_eq!("gh", suggestions.first().unwrap().value); assert_eq!("gh", suggestions.get(0).unwrap().value);
assert_eq!("api", suggestions.get(1).unwrap().value); assert_eq!("api", suggestions.get(1).unwrap().value);
assert_eq!("--", suggestions.get(2).unwrap().value); assert_eq!("--", suggestions.get(2).unwrap().value);
} }
@ -220,17 +170,16 @@ fn file_completions() {
let (dir, dir_str, engine, stack) = new_engine(); let (dir, dir_str, engine, stack) = new_engine();
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for the current folder // Test completions for the current folder
let target_dir = format!("cp {dir_str}{MAIN_SEPARATOR}"); let target_dir = format!("cp {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len()); let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values // Create the expected values
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
folder(dir.join("another")), folder(dir.join("another")),
file(dir.join("custom_completion.nu")), file(dir.join("custom_completion.nu")),
folder(dir.join("directory_completion")),
file(dir.join("nushell")), file(dir.join("nushell")),
folder(dir.join("test_a")), folder(dir.join("test_a")),
folder(dir.join("test_b")), folder(dir.join("test_b")),
@ -250,155 +199,13 @@ fn file_completions() {
// Match the results // Match the results
match_suggestions(expected_paths, suggestions); match_suggestions(expected_paths, suggestions);
// Test completions for hidden files
let target_dir = format!("ls {}/.", folder(dir.join(".hidden_folder")));
let suggestions = completer.complete(&target_dir, target_dir.len());
let expected_paths: Vec<String> =
vec![file(dir.join(".hidden_folder").join(".hidden_subfile"))];
// Match the results
match_suggestions(expected_paths, suggestions);
}
#[test]
fn partial_completions() {
// Create a new engine
let (dir, _, engine, stack) = new_partial_engine();
// Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
// Test completions for a folder's name
let target_dir = format!("cd {}", file(dir.join("pa")));
let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
folder(dir.join("partial_a")),
folder(dir.join("partial_b")),
folder(dir.join("partial_c")),
];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completions for the files whose name begin with "h"
// and are present under directories whose names begin with "pa"
let dir_str = file(dir.join("pa").join("h"));
let target_dir = format!("cp {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
file(dir.join("partial_a").join("have_ext.exe")),
file(dir.join("partial_a").join("have_ext.txt")),
file(dir.join("partial_a").join("hello")),
file(dir.join("partial_a").join("hola")),
file(dir.join("partial_b").join("hello_b")),
file(dir.join("partial_b").join("hi_b")),
file(dir.join("partial_c").join("hello_c")),
];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completion for all files under directories whose names begin with "pa"
let dir_str = folder(dir.join("pa"));
let target_dir = format!("ls {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
file(dir.join("partial_a").join("anotherfile")),
file(dir.join("partial_a").join("have_ext.exe")),
file(dir.join("partial_a").join("have_ext.txt")),
file(dir.join("partial_a").join("hello")),
file(dir.join("partial_a").join("hola")),
file(dir.join("partial_b").join("hello_b")),
file(dir.join("partial_b").join("hi_b")),
file(dir.join("partial_c").join("hello_c")),
];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completion for a single file
let dir_str = file(dir.join("fi").join("so"));
let target_dir = format!("rm {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![file(dir.join("final_partial").join("somefile"))];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completion where there is a sneaky `..` in the path
let dir_str = file(dir.join("par").join("..").join("fi").join("so"));
let target_dir = format!("rm {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
file(
dir.join("partial_a")
.join("..")
.join("final_partial")
.join("somefile"),
),
file(
dir.join("partial_b")
.join("..")
.join("final_partial")
.join("somefile"),
),
file(
dir.join("partial_c")
.join("..")
.join("final_partial")
.join("somefile"),
),
];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completion for all files under directories whose names begin with "pa"
let file_str = file(dir.join("partial_a").join("have"));
let target_file = format!("rm {file_str}");
let suggestions = completer.complete(&target_file, target_file.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
file(dir.join("partial_a").join("have_ext.exe")),
file(dir.join("partial_a").join("have_ext.txt")),
];
// Match the results
match_suggestions(expected_paths, suggestions);
// Test completion for all files under directories whose names begin with "pa"
let file_str = file(dir.join("partial_a").join("have_ext."));
let file_dir = format!("rm {file_str}");
let suggestions = completer.complete(&file_dir, file_dir.len());
// Create the expected values
let expected_paths: Vec<String> = vec![
file(dir.join("partial_a").join("have_ext.exe")),
file(dir.join("partial_a").join("have_ext.txt")),
];
// Match the results
match_suggestions(expected_paths, suggestions);
} }
#[test] #[test]
fn command_ls_with_filecompletion() { fn command_ls_with_filecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "ls "; let target_dir = "ls ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -407,7 +214,6 @@ fn command_ls_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -418,7 +224,6 @@ fn command_ls_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -426,20 +231,13 @@ fn command_ls_with_filecompletion() {
".hidden_folder/".to_string(), ".hidden_folder/".to_string(),
]; ];
match_suggestions(expected_paths, suggestions);
let target_dir = "ls custom_completion.";
let suggestions = completer.complete(target_dir, target_dir.len());
let expected_paths: Vec<String> = vec!["custom_completion.nu".to_string()];
match_suggestions(expected_paths, suggestions) match_suggestions(expected_paths, suggestions)
} }
#[test] #[test]
fn command_open_with_filecompletion() { fn command_open_with_filecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "open "; let target_dir = "open ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -448,7 +246,6 @@ fn command_open_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -459,7 +256,6 @@ fn command_open_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -467,13 +263,6 @@ fn command_open_with_filecompletion() {
".hidden_folder/".to_string(), ".hidden_folder/".to_string(),
]; ];
match_suggestions(expected_paths, suggestions);
let target_dir = "open custom_completion.";
let suggestions = completer.complete(target_dir, target_dir.len());
let expected_paths: Vec<String> = vec!["custom_completion.nu".to_string()];
match_suggestions(expected_paths, suggestions) match_suggestions(expected_paths, suggestions)
} }
@ -481,7 +270,7 @@ fn command_open_with_filecompletion() {
fn command_rm_with_globcompletion() { fn command_rm_with_globcompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "rm "; let target_dir = "rm ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -490,7 +279,6 @@ fn command_rm_with_globcompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -501,7 +289,6 @@ fn command_rm_with_globcompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -516,7 +303,7 @@ fn command_rm_with_globcompletion() {
fn command_cp_with_globcompletion() { fn command_cp_with_globcompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "cp "; let target_dir = "cp ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -525,7 +312,6 @@ fn command_cp_with_globcompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -536,7 +322,6 @@ fn command_cp_with_globcompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -551,7 +336,7 @@ fn command_cp_with_globcompletion() {
fn command_save_with_filecompletion() { fn command_save_with_filecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "save "; let target_dir = "save ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -560,7 +345,6 @@ fn command_save_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -571,7 +355,6 @@ fn command_save_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -586,7 +369,7 @@ fn command_save_with_filecompletion() {
fn command_touch_with_filecompletion() { fn command_touch_with_filecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "touch "; let target_dir = "touch ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -595,7 +378,6 @@ fn command_touch_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -606,7 +388,6 @@ fn command_touch_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -621,7 +402,7 @@ fn command_touch_with_filecompletion() {
fn command_watch_with_filecompletion() { fn command_watch_with_filecompletion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "watch "; let target_dir = "watch ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -630,7 +411,6 @@ fn command_watch_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -641,7 +421,6 @@ fn command_watch_with_filecompletion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -656,33 +435,16 @@ fn command_watch_with_filecompletion() {
fn file_completion_quoted() { fn file_completion_quoted() {
let (_, _, engine, stack) = new_quote_engine(); let (_, _, engine, stack) = new_quote_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "open "; let target_dir = "open ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"\'[a] bc.txt\'".to_string(),
"`--help`".to_string(),
"`-42`".to_string(),
"`-inf`".to_string(),
"`4.2`".to_string(),
"`te st.txt`".to_string(), "`te st.txt`".to_string(),
"`te#st.txt`".to_string(), "`te#st.txt`".to_string(),
"`te'st.txt`".to_string(), "`te'st.txt`".to_string(),
"`te(st).txt`".to_string(), "`te(st).txt`".to_string(),
format!("`{}`", folder("test dir".into())),
];
match_suggestions(expected_paths, suggestions);
let dir: PathBuf = "test dir".into();
let target_dir = format!("open '{}'", folder(dir.clone()));
let suggestions = completer.complete(&target_dir, target_dir.len());
let expected_paths: Vec<String> = vec![
format!("`{}`", file(dir.join("double quote"))),
format!("`{}`", file(dir.join("single quote"))),
]; ];
match_suggestions(expected_paths, suggestions) match_suggestions(expected_paths, suggestions)
@ -694,7 +456,7 @@ fn flag_completions() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for the 'ls' flags // Test completions for the 'ls' flags
let suggestions = completer.complete("ls -", 4); let suggestions = completer.complete("ls -", 4);
@ -729,16 +491,15 @@ fn folder_with_directorycompletions() {
let (dir, dir_str, engine, stack) = new_engine(); let (dir, dir_str, engine, stack) = new_engine();
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for the current folder // Test completions for the current folder
let target_dir = format!("cd {dir_str}{MAIN_SEPARATOR}"); let target_dir = format!("cd {dir_str}");
let suggestions = completer.complete(&target_dir, target_dir.len()); let suggestions = completer.complete(&target_dir, target_dir.len());
// Create the expected values // Create the expected values
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
folder(dir.join("another")), folder(dir.join("another")),
folder(dir.join("directory_completion")),
folder(dir.join("test_a")), folder(dir.join("test_a")),
folder(dir.join("test_b")), folder(dir.join("test_b")),
folder(dir.join(".hidden_folder")), folder(dir.join(".hidden_folder")),
@ -758,19 +519,16 @@ fn variables_completions() {
assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(record.as_bytes(), &mut engine, &mut stack, dir).is_ok());
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Test completions for $nu // Test completions for $nu
let suggestions = completer.complete("$nu.", 4); let suggestions = completer.complete("$nu.", 4);
assert_eq!(15, suggestions.len()); assert_eq!(12, suggestions.len());
let expected: Vec<String> = vec![ let expected: Vec<String> = vec![
"config-path".into(), "config-path".into(),
"current-exe".into(),
"default-config-dir".into(),
"env-path".into(), "env-path".into(),
"history-enabled".into(),
"history-path".into(), "history-path".into(),
"home-path".into(), "home-path".into(),
"is-interactive".into(), "is-interactive".into(),
@ -778,7 +536,7 @@ fn variables_completions() {
"loginshell-path".into(), "loginshell-path".into(),
"os-info".into(), "os-info".into(),
"pid".into(), "pid".into(),
"plugin-path".into(), "scope".into(),
"startup-time".into(), "startup-time".into(),
"temp-path".into(), "temp-path".into(),
]; ];
@ -789,29 +547,13 @@ fn variables_completions() {
// Test completions for $nu.h (filter) // Test completions for $nu.h (filter)
let suggestions = completer.complete("$nu.h", 5); let suggestions = completer.complete("$nu.h", 5);
assert_eq!(3, suggestions.len()); assert_eq!(2, suggestions.len());
let expected: Vec<String> = vec![ let expected: Vec<String> = vec!["history-path".into(), "home-path".into()];
"history-enabled".into(),
"history-path".into(),
"home-path".into(),
];
// Match results // Match results
match_suggestions(expected, suggestions); match_suggestions(expected, suggestions);
// Test completions for $nu.os-info
let suggestions = completer.complete("$nu.os-info.", 12);
assert_eq!(4, suggestions.len());
let expected: Vec<String> = vec![
"arch".into(),
"family".into(),
"kernel_version".into(),
"name".into(),
];
// Match results
match_suggestions(expected, suggestions);
// Test completions for custom var // Test completions for custom var
let suggestions = completer.complete("$actor.", 7); let suggestions = completer.complete("$actor.", 7);
@ -864,7 +606,7 @@ fn alias_of_command_and_flags() {
let alias = r#"alias ll = ls -l"#; let alias = r#"alias ll = ls -l"#;
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let suggestions = completer.complete("ll t", 4); let suggestions = completer.complete("ll t", 4);
#[cfg(windows)] #[cfg(windows)]
@ -883,7 +625,7 @@ fn alias_of_basic_command() {
let alias = r#"alias ll = ls "#; let alias = r#"alias ll = ls "#;
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let suggestions = completer.complete("ll t", 4); let suggestions = completer.complete("ll t", 4);
#[cfg(windows)] #[cfg(windows)]
@ -905,7 +647,7 @@ fn alias_of_another_alias() {
let alias = r#"alias lf = ll -f"#; let alias = r#"alias lf = ll -f"#;
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let suggestions = completer.complete("lf t", 4); let suggestions = completer.complete("lf t", 4);
#[cfg(windows)] #[cfg(windows)]
@ -916,30 +658,31 @@ fn alias_of_another_alias() {
match_suggestions(expected_paths, suggestions) match_suggestions(expected_paths, suggestions)
} }
fn run_external_completion(completer: &str, input: &str) -> Vec<Suggestion> { fn run_external_completion(block: &str, input: &str) -> Vec<Suggestion> {
let completer = format!("$env.config.completions.external.completer = {completer}");
// Create a new engine // Create a new engine
let (dir, _, mut engine_state, mut stack) = new_engine(); let (dir, _, mut engine_state, mut stack) = new_engine();
let (block, delta) = { let (_, delta) = {
let mut working_set = StateWorkingSet::new(&engine_state); let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, completer.as_bytes(), false); let (block, err) = parse(&mut working_set, None, block.as_bytes(), false, &[]);
assert!(working_set.parse_errors.is_empty()); assert!(err.is_none());
(block, working_set.render()) (block, working_set.render())
}; };
assert!(engine_state.merge_delta(delta).is_ok()); assert!(engine_state.merge_delta(delta).is_ok());
assert!(
eval_block::<WithoutDebug>(&engine_state, &mut stack, &block, PipelineData::Empty).is_ok()
);
// Merge environment into the permanent state // Merge environment into the permanent state
assert!(engine_state.merge_env(&mut stack, &dir).is_ok()); 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);
// Instantiate a new completer // Instantiate a new completer
let mut completer = NuCompleter::new(Arc::new(engine_state), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack);
completer.complete(input, input.len()) completer.complete(input, input.len())
} }
@ -948,7 +691,7 @@ fn run_external_completion(completer: &str, input: &str) -> Vec<Suggestion> {
fn unknown_command_completion() { fn unknown_command_completion() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let target_dir = "thiscommanddoesnotexist "; let target_dir = "thiscommanddoesnotexist ";
let suggestions = completer.complete(target_dir, target_dir.len()); let suggestions = completer.complete(target_dir, target_dir.len());
@ -957,7 +700,6 @@ fn unknown_command_completion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -968,7 +710,6 @@ fn unknown_command_completion() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -1011,7 +752,7 @@ fn flagcompletion_triggers_after_cursor_piped(mut completer: NuCompleter) {
fn filecompletions_triggers_after_cursor() { fn filecompletions_triggers_after_cursor() {
let (_, _, engine, stack) = new_engine(); let (_, _, engine, stack) = new_engine();
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
let suggestions = completer.complete("cp test_c", 3); let suggestions = completer.complete("cp test_c", 3);
@ -1019,7 +760,6 @@ fn filecompletions_triggers_after_cursor() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another\\".to_string(), "another\\".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion\\".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a\\".to_string(), "test_a\\".to_string(),
"test_b\\".to_string(), "test_b\\".to_string(),
@ -1030,7 +770,6 @@ fn filecompletions_triggers_after_cursor() {
let expected_paths: Vec<String> = vec![ let expected_paths: Vec<String> = vec![
"another/".to_string(), "another/".to_string(),
"custom_completion.nu".to_string(), "custom_completion.nu".to_string(),
"directory_completion/".to_string(),
"nushell".to_string(), "nushell".to_string(),
"test_a/".to_string(), "test_a/".to_string(),
"test_b/".to_string(), "test_b/".to_string(),
@ -1083,34 +822,6 @@ fn extern_complete_flags(mut extern_completer: NuCompleter) {
match_suggestions(expected, suggestions); match_suggestions(expected, suggestions);
} }
#[rstest]
fn custom_completer_triggers_cursor_before_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 8);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "".into()];
match_suggestions(expected, suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_on_word_left_boundary(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 8);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "".into()];
match_suggestions(expected, suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_next_to_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar", 11);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "bar".into()];
match_suggestions(expected, suggestions);
}
#[rstest]
fn custom_completer_triggers_cursor_after_word(mut custom_completer: NuCompleter) {
let suggestions = custom_completer.complete("cmd foo bar ", 12);
let expected: Vec<String> = vec!["cmd".into(), "foo".into(), "bar".into(), "".into()];
match_suggestions(expected, suggestions);
}
#[ignore = "was reverted, still needs fixing"] #[ignore = "was reverted, still needs fixing"]
#[rstest] #[rstest]
fn alias_offset_bug_7648() { fn alias_offset_bug_7648() {
@ -1120,7 +831,7 @@ fn alias_offset_bug_7648() {
let alias = r#"alias ea = ^$env.EDITOR /tmp/test.s"#; let alias = r#"alias ea = ^$env.EDITOR /tmp/test.s"#;
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Issue #7648 // Issue #7648
// Nushell crashes when an alias name is shorter than the alias command // Nushell crashes when an alias name is shorter than the alias command
@ -1139,7 +850,7 @@ fn alias_offset_bug_7754() {
let alias = r#"alias ll = ls -l"#; let alias = r#"alias ll = ls -l"#;
assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok()); assert!(support::merge_input(alias.as_bytes(), &mut engine, &mut stack, dir).is_ok());
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack);
// Issue #7754 // Issue #7754
// Nushell crashes when an alias name is shorter than the alias command // Nushell crashes when an alias name is shorter than the alias command

View File

@ -1,2 +0,0 @@
mod commands;
mod completions;

View File

@ -1,64 +1,63 @@
use std::path::PathBuf;
use nu_command::create_default_context;
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
PipelineData, ShellError, Span, Value, PipelineData, ShellError, Span, Value,
}; };
use nu_test_support::fs; use nu_test_support::fs;
use reedline::Suggestion; use reedline::Suggestion;
use std::path::{PathBuf, MAIN_SEPARATOR}; const SEP: char = std::path::MAIN_SEPARATOR;
fn create_default_context() -> EngineState {
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
}
// creates a new engine with the current path into the completions fixtures folder // creates a new engine with the current path into the completions fixtures folder
pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("completions"); let dir = fs::fixtures().join("completions");
let dir_str = dir let mut dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
// Add $nu
engine_state.generate_nu_constant();
// New stack // New stack
let mut stack = Stack::new(); let mut stack = Stack::new();
// Add pwd as env var // Add pwd as env var
stack.add_env_var( stack.add_env_var(
"PWD".to_string(), "PWD".to_string(),
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())), Value::String {
val: dir_str.clone(),
span: nu_protocol::Span::new(0, dir_str.len()),
},
); );
stack.add_env_var( stack.add_env_var(
"TEST".to_string(), "TEST".to_string(),
Value::string( Value::String {
"NUSHELL".to_string(), val: "NUSHELL".to_string(),
nu_protocol::Span::new(0, dir_str.len()), span: nu_protocol::Span::new(0, dir_str.len()),
), },
); );
#[cfg(windows)] #[cfg(windows)]
stack.add_env_var( stack.add_env_var(
"Path".to_string(), "Path".to_string(),
Value::string( Value::String {
"c:\\some\\path;c:\\some\\other\\path".to_string(), val: "c:\\some\\path;c:\\some\\other\\path".to_string(),
nu_protocol::Span::new(0, dir_str.len()), span: nu_protocol::Span::new(0, dir_str.len()),
), },
); );
#[cfg(not(windows))] #[cfg(not(windows))]
stack.add_env_var( stack.add_env_var(
"PATH".to_string(), "PATH".to_string(),
Value::string( Value::String {
"/some/path:/some/other/path".to_string(), val: "/some/path:/some/other/path".to_string(),
nu_protocol::Span::new(0, dir_str.len()), span: nu_protocol::Span::new(0, dir_str.len()),
), },
); );
// Merge environment into the permanent state // Merge environment into the permanent state
@ -71,11 +70,12 @@ pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets // Target folder inside assets
let dir = fs::fixtures().join("quoted_completions"); let dir = fs::fixtures().join("quoted_completions");
let dir_str = dir let mut dir_str = dir
.clone() .clone()
.into_os_string() .into_os_string()
.into_string() .into_string()
.unwrap_or_default(); .unwrap_or_default();
dir_str.push(SEP);
// Create a new engine with default context // Create a new engine with default context
let mut engine_state = create_default_context(); let mut engine_state = create_default_context();
@ -86,49 +86,17 @@ pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
// Add pwd as env var // Add pwd as env var
stack.add_env_var( stack.add_env_var(
"PWD".to_string(), "PWD".to_string(),
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())), Value::String {
val: dir_str.clone(),
span: nu_protocol::Span::new(0, dir_str.len()),
},
); );
stack.add_env_var( stack.add_env_var(
"TEST".to_string(), "TEST".to_string(),
Value::string( Value::String {
"NUSHELL".to_string(), val: "NUSHELL".to_string(),
nu_protocol::Span::new(0, dir_str.len()), span: nu_protocol::Span::new(0, dir_str.len()),
), },
);
// Merge environment into the permanent state
let merge_result = engine_state.merge_env(&mut stack, &dir);
assert!(merge_result.is_ok());
(dir, dir_str, engine_state, stack)
}
pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) {
// Target folder inside assets
let dir = fs::fixtures().join("partial_completions");
let dir_str = dir
.clone()
.into_os_string()
.into_string()
.unwrap_or_default();
// Create a new engine with default context
let mut engine_state = create_default_context();
// New stack
let mut stack = Stack::new();
// Add pwd as env var
stack.add_env_var(
"PWD".to_string(),
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())),
);
stack.add_env_var(
"TEST".to_string(),
Value::string(
"NUSHELL".to_string(),
nu_protocol::Span::new(0, dir_str.len()),
),
); );
// Merge environment into the permanent state // Merge environment into the permanent state
@ -157,7 +125,7 @@ pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
// append the separator to the converted path // append the separator to the converted path
pub fn folder(path: PathBuf) -> String { pub fn folder(path: PathBuf) -> String {
let mut converted_path = file(path); let mut converted_path = file(path);
converted_path.push(MAIN_SEPARATOR); converted_path.push(SEP);
converted_path converted_path
} }
@ -178,20 +146,27 @@ pub fn merge_input(
let (block, delta) = { let (block, delta) = {
let mut working_set = StateWorkingSet::new(engine_state); let mut working_set = StateWorkingSet::new(engine_state);
let block = parse(&mut working_set, None, input, false); let (block, err) = parse(&mut working_set, None, input, false, &[]);
assert!(working_set.parse_errors.is_empty()); assert!(err.is_none());
(block, working_set.render()) (block, working_set.render())
}; };
engine_state.merge_delta(delta)?; engine_state.merge_delta(delta)?;
assert!(eval_block::<WithoutDebug>( assert!(eval_block(
engine_state, engine_state,
stack, stack,
&block, &block,
PipelineData::Value(Value::nothing(Span::unknown()), None), PipelineData::Value(
Value::Nothing {
span: Span::unknown(),
},
None
),
false,
false
) )
.is_ok()); .is_ok());

View File

@ -1,21 +0,0 @@
[package]
authors = ["The Nushell Project Developers"]
description = "The foundation tools to build Nushell commands."
edition = "2021"
license = "MIT"
name = "nu-cmd-base"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-base"
version = "0.94.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.94.0" }
nu-parser = { path = "../nu-parser", version = "0.94.0" }
nu-path = { path = "../nu-path", version = "0.94.0" }
nu-protocol = { path = "../nu-protocol", version = "0.94.0" }
indexmap = { workspace = true }
miette = { workspace = true }
[dev-dependencies]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +0,0 @@
pub mod to;

View File

@ -1,20 +0,0 @@
use indexmap::{indexset, IndexSet};
use nu_protocol::Value;
pub fn merge_descriptors(values: &[Value]) -> Vec<String> {
let mut ret: Vec<String> = vec![];
let mut seen: IndexSet<String> = indexset! {};
for value in values {
let data_descriptors = match value {
Value::Record { val, .. } => val.columns().cloned().collect(),
_ => vec!["".to_string()],
};
for desc in data_descriptors {
if !desc.is_empty() && !seen.contains(&desc) {
seen.insert(desc.to_string());
ret.push(desc.to_string());
}
}
}
ret
}

View File

@ -1 +0,0 @@
pub mod delimited;

View File

@ -1,351 +0,0 @@
use crate::util::get_guaranteed_cwd;
use miette::Result;
use nu_engine::{eval_block, eval_block_with_early_return};
use nu_parser::parse;
use nu_protocol::{
cli_error::{report_error, report_error_new},
debugger::WithoutDebug,
engine::{Closure, EngineState, Stack, StateWorkingSet},
PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
};
use std::sync::Arc;
pub fn eval_env_change_hook(
env_change_hook: Option<Value>,
engine_state: &mut EngineState,
stack: &mut Stack,
) -> Result<(), ShellError> {
if let Some(hook) = env_change_hook {
match hook {
Value::Record { val, .. } => {
for (env_name, hook_value) in &*val {
let before = engine_state
.previous_env_vars
.get(env_name)
.cloned()
.unwrap_or_default();
let after = stack
.get_env_var(engine_state, env_name)
.unwrap_or_default();
if before != after {
eval_hook(
engine_state,
stack,
None,
vec![("$before".into(), before), ("$after".into(), after.clone())],
hook_value,
"env_change",
)?;
Arc::make_mut(&mut engine_state.previous_env_vars)
.insert(env_name.to_string(), after);
}
}
}
x => {
return Err(ShellError::TypeMismatch {
err_message: "record for the 'env_change' hook".to_string(),
span: x.span(),
});
}
}
}
Ok(())
}
pub fn eval_hook(
engine_state: &mut EngineState,
stack: &mut Stack,
input: Option<PipelineData>,
arguments: Vec<(String, Value)>,
value: &Value,
hook_name: &str,
) -> Result<PipelineData, ShellError> {
let mut output = PipelineData::empty();
let span = value.span();
match value {
Value::String { val, .. } => {
let (block, delta, vars) = {
let mut working_set = StateWorkingSet::new(engine_state);
let mut vars: Vec<(VarId, Value)> = vec![];
for (name, val) in arguments {
let var_id = working_set.add_variable(
name.as_bytes().to_vec(),
val.span(),
Type::Any,
false,
);
vars.push((var_id, val));
}
let output = parse(
&mut working_set,
Some(&format!("{hook_name} hook")),
val.as_bytes(),
false,
);
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
return Err(ShellError::UnsupportedConfigValue {
expected: "valid source code".into(),
value: "source code with syntax errors".into(),
span,
});
}
(output, working_set.render(), vars)
};
engine_state.merge_delta(delta)?;
let input = if let Some(input) = input {
input
} else {
PipelineData::empty()
};
let var_ids: Vec<VarId> = vars
.into_iter()
.map(|(var_id, val)| {
stack.add_var(var_id, val);
var_id
})
.collect();
match eval_block::<WithoutDebug>(engine_state, stack, &block, input) {
Ok(pipeline_data) => {
output = pipeline_data;
}
Err(err) => {
report_error_new(engine_state, &err);
}
}
for var_id in var_ids.iter() {
stack.remove_var(*var_id);
}
}
Value::List { vals, .. } => {
for val in vals {
eval_hook(
engine_state,
stack,
None,
arguments.clone(),
val,
&format!("{hook_name} list, recursive"),
)?;
}
}
Value::Record { val, .. } => {
// Hooks can optionally be a record in this form:
// {
// condition: {|before, after| ... } # block that evaluates to true/false
// code: # block or a string
// }
// The condition block will be run to check whether the main hook (in `code`) should be run.
// If it returns true (the default if a condition block is not specified), the hook should be run.
let do_run_hook = if let Some(condition) = val.get("condition") {
let other_span = condition.span();
if let Ok(closure) = condition.as_closure() {
match run_hook(
engine_state,
stack,
closure,
None,
arguments.clone(),
other_span,
) {
Ok(pipeline_data) => {
if let PipelineData::Value(Value::Bool { val, .. }, ..) = pipeline_data
{
val
} else {
return Err(ShellError::UnsupportedConfigValue {
expected: "boolean output".to_string(),
value: "other PipelineData variant".to_string(),
span: other_span,
});
}
}
Err(err) => {
return Err(err);
}
}
} else {
return Err(ShellError::UnsupportedConfigValue {
expected: "block".to_string(),
value: format!("{}", condition.get_type()),
span: other_span,
});
}
} else {
// always run the hook
true
};
if do_run_hook {
let Some(follow) = val.get("code") else {
return Err(ShellError::CantFindColumn {
col_name: "code".into(),
span,
src_span: span,
});
};
let source_span = follow.span();
match follow {
Value::String { val, .. } => {
let (block, delta, vars) = {
let mut working_set = StateWorkingSet::new(engine_state);
let mut vars: Vec<(VarId, Value)> = vec![];
for (name, val) in arguments {
let var_id = working_set.add_variable(
name.as_bytes().to_vec(),
val.span(),
Type::Any,
false,
);
vars.push((var_id, val));
}
let output = parse(
&mut working_set,
Some(&format!("{hook_name} hook")),
val.as_bytes(),
false,
);
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
return Err(ShellError::UnsupportedConfigValue {
expected: "valid source code".into(),
value: "source code with syntax errors".into(),
span: source_span,
});
}
(output, working_set.render(), vars)
};
engine_state.merge_delta(delta)?;
let input = PipelineData::empty();
let var_ids: Vec<VarId> = vars
.into_iter()
.map(|(var_id, val)| {
stack.add_var(var_id, val);
var_id
})
.collect();
match eval_block::<WithoutDebug>(engine_state, stack, &block, input) {
Ok(pipeline_data) => {
output = pipeline_data;
}
Err(err) => {
report_error_new(engine_state, &err);
}
}
for var_id in var_ids.iter() {
stack.remove_var(*var_id);
}
}
Value::Closure { val, .. } => {
run_hook(engine_state, stack, val, input, arguments, source_span)?;
}
other => {
return Err(ShellError::UnsupportedConfigValue {
expected: "block or string".to_string(),
value: format!("{}", other.get_type()),
span: source_span,
});
}
}
}
}
Value::Closure { val, .. } => {
output = run_hook(engine_state, stack, val, input, arguments, span)?;
}
other => {
return Err(ShellError::UnsupportedConfigValue {
expected: "string, block, record, or list of commands".into(),
value: format!("{}", other.get_type()),
span: other.span(),
});
}
}
let cwd = get_guaranteed_cwd(engine_state, stack);
engine_state.merge_env(stack, cwd)?;
Ok(output)
}
fn run_hook(
engine_state: &EngineState,
stack: &mut Stack,
closure: &Closure,
optional_input: Option<PipelineData>,
arguments: Vec<(String, Value)>,
span: Span,
) -> Result<PipelineData, ShellError> {
let block = engine_state.get_block(closure.block_id);
let input = optional_input.unwrap_or_else(PipelineData::empty);
let mut callee_stack = stack
.captures_to_stack_preserve_out_dest(closure.captures.clone())
.reset_pipes();
for (idx, PositionalArg { var_id, .. }) in
block.signature.required_positional.iter().enumerate()
{
if let Some(var_id) = var_id {
if let Some(arg) = arguments.get(idx) {
callee_stack.add_var(*var_id, arg.1.clone())
} else {
return Err(ShellError::IncompatibleParametersSingle {
msg: "This hook block has too many parameters".into(),
span,
});
}
}
}
let pipeline_data = eval_block_with_early_return::<WithoutDebug>(
engine_state,
&mut callee_stack,
block,
input,
)?;
if let PipelineData::Value(Value::Error { error, .. }, _) = pipeline_data {
return Err(*error);
}
// If all went fine, preserve the environment of the called block
let caller_env_vars = stack.get_env_var_names(engine_state);
// remove env vars that are present in the caller but not in the callee
// (the callee hid them)
for var in caller_env_vars.iter() {
if !callee_stack.has_env_var(engine_state, var) {
stack.remove_env_var(engine_state, var);
}
}
// add new env vars from callee to caller
for (var, value) in callee_stack.get_stack_env_vars() {
stack.add_env_var(var, value);
}
Ok(pipeline_data)
}

View File

@ -1,4 +0,0 @@
pub mod formats;
pub mod hook;
pub mod input_handler;
pub mod util;

View File

@ -1,109 +0,0 @@
use nu_protocol::{
engine::{EngineState, Stack},
Range, ShellError, Span, Value,
};
use std::{ops::Bound, path::PathBuf};
pub fn get_init_cwd() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| {
std::env::var("PWD")
.map(Into::into)
.unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default())
})
}
pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf {
engine_state
.cwd(Some(stack))
.unwrap_or(crate::util::get_init_cwd())
}
type MakeRangeError = fn(&str, Span) -> ShellError;
pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
match range {
Range::IntRange(range) => {
let start = range.start().try_into().unwrap_or(0);
let end = match range.end() {
Bound::Included(v) => (v + 1) as isize,
Bound::Excluded(v) => v as isize,
Bound::Unbounded => isize::MAX,
};
Ok((start, end))
}
Range::FloatRange(_) => Err(|msg, span| ShellError::TypeMismatch {
err_message: msg.to_string(),
span,
}),
}
}
const HELP_MSG: &str = "Nushell's config file can be found with the command: $nu.config-path. \
For more help: (https://nushell.sh/book/configuration.html#configurations-with-built-in-commands)";
fn get_editor_commandline(
value: &Value,
var_name: &str,
) -> Result<(String, Vec<String>), ShellError> {
match value {
Value::String { val, .. } if !val.is_empty() => Ok((val.to_string(), Vec::new())),
Value::List { vals, .. } if !vals.is_empty() => {
let mut editor_cmd = vals.iter().map(|l| l.coerce_string());
match editor_cmd.next().transpose()? {
Some(editor) if !editor.is_empty() => {
let params = editor_cmd.collect::<Result<_, ShellError>>()?;
Ok((editor, params))
}
_ => Err(ShellError::GenericError {
error: "Editor executable is missing".into(),
msg: "Set the first element to an executable".into(),
span: Some(value.span()),
help: Some(HELP_MSG.into()),
inner: vec![],
}),
}
}
Value::String { .. } | Value::List { .. } => Err(ShellError::GenericError {
error: format!("{var_name} should be a non-empty string or list<String>"),
msg: "Specify an executable here".into(),
span: Some(value.span()),
help: Some(HELP_MSG.into()),
inner: vec![],
}),
x => Err(ShellError::CantConvert {
to_type: "string or list<string>".into(),
from_type: x.get_type().to_string(),
span: value.span(),
help: None,
}),
}
}
pub fn get_editor(
engine_state: &EngineState,
stack: &Stack,
span: Span,
) -> Result<(String, Vec<String>), ShellError> {
let config = engine_state.get_config();
let env_vars = stack.get_env_vars(engine_state);
if let Ok(buff_editor) =
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
{
Ok(buff_editor)
} else if let Some(value) = env_vars.get("EDITOR") {
get_editor_commandline(value, "$env.EDITOR")
} else if let Some(value) = env_vars.get("VISUAL") {
get_editor_commandline(value, "$env.VISUAL")
} else {
Err(ShellError::GenericError {
error: "No editor configured".into(),
msg:
"Please specify one via `$env.config.buffer_editor` or `$env.EDITOR`/`$env.VISUAL`"
.into(),
span: Some(span),
help: Some(HELP_MSG.into()),
inner: vec![],
})
}
}

View File

@ -1,42 +0,0 @@
[package]
authors = ["The Nushell Project Developers"]
description = "Nushell's extra commands that are not part of the 1.0 api standard."
edition = "2021"
license = "MIT"
name = "nu-cmd-extra"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-extra"
version = "0.94.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
bench = false
[dependencies]
nu-cmd-base = { path = "../nu-cmd-base", version = "0.94.0" }
nu-engine = { path = "../nu-engine", version = "0.94.0" }
nu-json = { version = "0.94.0", path = "../nu-json" }
nu-parser = { path = "../nu-parser", version = "0.94.0" }
nu-pretty-hex = { version = "0.94.0", path = "../nu-pretty-hex" }
nu-protocol = { path = "../nu-protocol", version = "0.94.0" }
nu-utils = { path = "../nu-utils", version = "0.94.0" }
# Potential dependencies for extras
heck = { workspace = true }
num-traits = { workspace = true }
nu-ansi-term = { workspace = true }
fancy-regex = { workspace = true }
rust-embed = { workspace = true }
serde = { workspace = true }
serde_urlencoded = { workspace = true }
v_htmlescape = { workspace = true }
itertools = { workspace = true }
[features]
extra = ["default"]
default = []
[dev-dependencies]
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.94.0" }
nu-command = { path = "../nu-command", version = "0.94.0" }
nu-test-support = { path = "../nu-test-support", version = "0.94.0" }

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,13 +0,0 @@
# nu-cmd-extra
The commands in this crate are the *extra commands* of Nushell. These commands
are not in a state to be guaranteed to be part of the 1.0 API; meaning that
there is no guarantee longer term that these commands will be around into the
future.
For a while we did exclude them behind the `--features extra` compile time
flag, meaning that the default release did not contain them. As we (the Nushell
team) shipped a full build including both `extra` and `dataframe` for some
time, we chose to sunset the `extra` feature but keep the commands in this
crate for now. In the future the commands may be moved to more topical crates
or discarded into plugins.

View File

@ -1,77 +0,0 @@
#[cfg(test)]
use nu_protocol::engine::Command;
#[cfg(test)]
pub fn test_examples(cmd: impl Command + 'static) {
test_examples::test_examples(cmd);
}
#[cfg(test)]
mod test_examples {
use nu_cmd_lang::example_support::{
check_all_signature_input_output_types_entries_have_examples,
check_example_evaluates_to_expected_output,
check_example_input_and_output_types_match_command_signature,
};
use nu_protocol::{
engine::{Command, EngineState, StateWorkingSet},
Type,
};
use std::collections::HashSet;
pub fn test_examples(cmd: impl Command + 'static) {
let examples = cmd.examples();
let signature = cmd.signature();
let mut engine_state = make_engine_state(cmd.clone_box());
let cwd = std::env::current_dir().expect("Could not get current working directory.");
let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new();
for example in examples {
if example.result.is_none() {
continue;
}
witnessed_type_transformations.extend(
check_example_input_and_output_types_match_command_signature(
&example,
&cwd,
&mut make_engine_state(cmd.clone_box()),
&signature.input_output_types,
signature.operates_on_cell_paths(),
),
);
check_example_evaluates_to_expected_output(&example, cwd.as_path(), &mut engine_state);
}
check_all_signature_input_output_types_entries_have_examples(
signature,
witnessed_type_transformations,
);
}
fn make_engine_state(cmd: Box<dyn Command>) -> Box<EngineState> {
let mut engine_state = Box::new(EngineState::new());
let delta = {
// Base functions that are needed for testing
// Try to keep this working set small to keep tests running as fast as possible
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(nu_command::Enumerate));
working_set.add_decl(Box::new(nu_cmd_lang::If));
working_set.add_decl(Box::new(nu_command::MathRound));
// Adding the command that is being tested to the working set
working_set.add_decl(cmd);
working_set.render()
};
engine_state
.merge_delta(delta)
.expect("Error merging delta");
engine_state
}
}

View File

@ -1,140 +0,0 @@
use super::binary_op;
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct BitsAnd;
impl Command for BitsAnd {
fn name(&self) -> &str {
"bits and"
}
fn signature(&self) -> Signature {
Signature::build("bits and")
.input_output_types(vec![
(Type::Int, Type::Int),
(Type::Binary, Type::Binary),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
(
Type::List(Box::new(Type::Binary)),
Type::List(Box::new(Type::Binary)),
),
])
.required(
"target",
SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::Int]),
"right-hand side of the operation",
)
.named(
"endian",
SyntaxShape::String,
"byte encode endian, available options: native(default), little, big",
Some('e'),
)
.category(Category::Bits)
}
fn usage(&self) -> &str {
"Performs bitwise and for ints or binary values."
}
fn search_terms(&self) -> Vec<&str> {
vec!["logic and"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let target: Value = call.req(engine_state, stack, 0)?;
let endian = call.get_flag::<Spanned<String>>(engine_state, stack, "endian")?;
let little_endian = if let Some(endian) = endian {
match endian.item.as_str() {
"native" => cfg!(target_endian = "little"),
"little" => true,
"big" => false,
_ => {
return Err(ShellError::TypeMismatch {
err_message: "Endian must be one of native, little, big".to_string(),
span: endian.span,
})
}
}
} else {
cfg!(target_endian = "little")
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| binary_op(&value, &target, little_endian, |(l, r)| l & r, head),
engine_state.ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Apply bitwise and to two numbers",
example: "2 | bits and 2",
result: Some(Value::test_int(2)),
},
Example {
description: "Apply bitwise and to two binary values",
example: "0x[ab cd] | bits and 0x[99 99]",
result: Some(Value::test_binary([0x89, 0x89])),
},
Example {
description: "Apply bitwise and to a list of numbers",
example: "[4 3 2] | bits and 2",
result: Some(Value::test_list(vec![
Value::test_int(0),
Value::test_int(2),
Value::test_int(2),
])),
},
Example {
description: "Apply bitwise and to a list of binary data",
example: "[0x[7f ff] 0x[ff f0]] | bits and 0x[99 99]",
result: Some(Value::test_list(vec![
Value::test_binary([0x19, 0x99]),
Value::test_binary([0x99, 0x90]),
])),
},
Example {
description:
"Apply bitwise and to binary data of varying lengths with specified endianness",
example: "0x[c0 ff ee] | bits and 0x[ff] --endian big",
result: Some(Value::test_binary(vec![0x00, 0x00, 0xee])),
},
Example {
description: "Apply bitwise and to input binary data smaller than the operand",
example: "0x[ff] | bits and 0x[12 34 56] --endian little",
result: Some(Value::test_binary(vec![0x12, 0x00, 0x00])),
},
]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(BitsAnd {})
}
}

View File

@ -1,210 +0,0 @@
use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::command_prelude::*;
use num_traits::ToPrimitive;
pub struct Arguments {
cell_paths: Option<Vec<CellPath>>,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
self.cell_paths.take()
}
}
#[derive(Clone)]
pub struct BitsInto;
impl Command for BitsInto {
fn name(&self) -> &str {
"into bits"
}
fn signature(&self) -> Signature {
Signature::build("into bits")
.input_output_types(vec![
(Type::Binary, Type::String),
(Type::Int, Type::String),
(Type::Filesize, Type::String),
(Type::Duration, Type::String),
(Type::String, Type::String),
(Type::Bool, Type::String),
(Type::table(), Type::table()),
(Type::record(), Type::record()),
])
.allow_variants_without_examples(true) // TODO: supply exhaustive examples
.rest(
"rest",
SyntaxShape::CellPath,
"for a data structure input, convert data at the given cell paths",
)
.category(Category::Conversions)
}
fn usage(&self) -> &str {
"Convert value to a binary primitive."
}
fn search_terms(&self) -> Vec<&str> {
vec!["convert", "cast"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
into_bits(engine_state, stack, call, input)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "convert a binary value into a string, padded to 8 places with 0s",
example: "0x[1] | into bits",
result: Some(Value::string("00000001",
Span::test_data(),
)),
},
Example {
description: "convert an int into a string, padded to 8 places with 0s",
example: "1 | into bits",
result: Some(Value::string("00000001",
Span::test_data(),
)),
},
Example {
description: "convert a filesize value into a string, padded to 8 places with 0s",
example: "1b | into bits",
result: Some(Value::string("00000001",
Span::test_data(),
)),
},
Example {
description: "convert a duration value into a string, padded to 8 places with 0s",
example: "1ns | into bits",
result: Some(Value::string("00000001",
Span::test_data(),
)),
},
Example {
description: "convert a boolean value into a string, padded to 8 places with 0s",
example: "true | into bits",
result: Some(Value::string("00000001",
Span::test_data(),
)),
},
Example {
description: "convert a string into a raw binary string, padded with 0s to 8 places",
example: "'nushell.sh' | into bits",
result: Some(Value::string("01101110 01110101 01110011 01101000 01100101 01101100 01101100 00101110 01110011 01101000",
Span::test_data(),
)),
},
]
}
}
fn into_bits(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let cell_paths = call.rest(engine_state, stack, 0)?;
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
if let PipelineData::ByteStream(stream, ..) = input {
// TODO: in the future, we may want this to stream out, converting each to bytes
Ok(Value::binary(stream.into_bytes()?, head).into_pipeline_data())
} else {
let args = Arguments { cell_paths };
operate(action, args, input, call.head, engine_state.ctrlc.clone())
}
}
fn convert_to_smallest_number_type(num: i64, span: Span) -> Value {
if let Some(v) = num.to_i8() {
let bytes = v.to_ne_bytes();
let mut raw_string = "".to_string();
for ch in bytes {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
} else if let Some(v) = num.to_i16() {
let bytes = v.to_ne_bytes();
let mut raw_string = "".to_string();
for ch in bytes {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
} else if let Some(v) = num.to_i32() {
let bytes = v.to_ne_bytes();
let mut raw_string = "".to_string();
for ch in bytes {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
} else {
let bytes = num.to_ne_bytes();
let mut raw_string = "".to_string();
for ch in bytes {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
}
}
pub fn action(input: &Value, _args: &Arguments, span: Span) -> Value {
match input {
Value::Binary { val, .. } => {
let mut raw_string = "".to_string();
for ch in val {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
}
Value::Int { val, .. } => convert_to_smallest_number_type(*val, span),
Value::Filesize { val, .. } => convert_to_smallest_number_type(*val, span),
Value::Duration { val, .. } => convert_to_smallest_number_type(*val, span),
Value::String { val, .. } => {
let raw_bytes = val.as_bytes();
let mut raw_string = "".to_string();
for ch in raw_bytes {
raw_string.push_str(&format!("{:08b} ", ch));
}
Value::string(raw_string.trim(), span)
}
Value::Bool { val, .. } => {
let v = <i64 as From<bool>>::from(*val);
convert_to_smallest_number_type(v, span)
}
// Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => input.clone(),
other => Value::error(
ShellError::OnlySupportsThisInputType {
exp_input_type: "int, filesize, string, duration, binary, or bool".into(),
wrong_type: other.get_type().to_string(),
dst_span: span,
src_span: other.span(),
},
span,
),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(BitsInto {})
}
}

View File

@ -1,166 +0,0 @@
mod and;
mod bits_;
mod into;
mod not;
mod or;
mod rotate_left;
mod rotate_right;
mod shift_left;
mod shift_right;
mod xor;
pub use and::BitsAnd;
pub use bits_::Bits;
pub use into::BitsInto;
pub use not::BitsNot;
pub use or::BitsOr;
pub use rotate_left::BitsRol;
pub use rotate_right::BitsRor;
pub use shift_left::BitsShl;
pub use shift_right::BitsShr;
pub use xor::BitsXor;
use nu_protocol::{ShellError, Span, Spanned, Value};
use std::iter;
#[derive(Clone, Copy)]
enum NumberBytes {
One,
Two,
Four,
Eight,
Auto,
}
#[derive(Clone, Copy)]
enum InputNumType {
One,
Two,
Four,
Eight,
SignedOne,
SignedTwo,
SignedFour,
SignedEight,
}
fn get_number_bytes(
number_bytes: Option<Spanned<usize>>,
head: Span,
) -> Result<NumberBytes, ShellError> {
match number_bytes {
None => Ok(NumberBytes::Auto),
Some(Spanned { item: 1, .. }) => Ok(NumberBytes::One),
Some(Spanned { item: 2, .. }) => Ok(NumberBytes::Two),
Some(Spanned { item: 4, .. }) => Ok(NumberBytes::Four),
Some(Spanned { item: 8, .. }) => Ok(NumberBytes::Eight),
Some(Spanned { span, .. }) => Err(ShellError::UnsupportedInput {
msg: "Only 1, 2, 4, or 8 bytes are supported as word sizes".to_string(),
input: "value originates from here".to_string(),
msg_span: head,
input_span: span,
}),
}
}
fn get_input_num_type(val: i64, signed: bool, number_size: NumberBytes) -> InputNumType {
if signed || val < 0 {
match number_size {
NumberBytes::One => InputNumType::SignedOne,
NumberBytes::Two => InputNumType::SignedTwo,
NumberBytes::Four => InputNumType::SignedFour,
NumberBytes::Eight => InputNumType::SignedEight,
NumberBytes::Auto => {
if val <= 0x7F && val >= -(2i64.pow(7)) {
InputNumType::SignedOne
} else if val <= 0x7FFF && val >= -(2i64.pow(15)) {
InputNumType::SignedTwo
} else if val <= 0x7FFFFFFF && val >= -(2i64.pow(31)) {
InputNumType::SignedFour
} else {
InputNumType::SignedEight
}
}
}
} else {
match number_size {
NumberBytes::One => InputNumType::One,
NumberBytes::Two => InputNumType::Two,
NumberBytes::Four => InputNumType::Four,
NumberBytes::Eight => InputNumType::Eight,
NumberBytes::Auto => {
if val <= 0xFF {
InputNumType::One
} else if val <= 0xFFFF {
InputNumType::Two
} else if val <= 0xFFFFFFFF {
InputNumType::Four
} else {
InputNumType::Eight
}
}
}
}
}
fn binary_op<F>(lhs: &Value, rhs: &Value, little_endian: bool, f: F, head: Span) -> Value
where
F: Fn((i64, i64)) -> i64,
{
let span = lhs.span();
match (lhs, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Value::int(f((*lhs, *rhs)), span)
}
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => {
let (lhs, rhs, max_len, min_len) = match (lhs.len(), rhs.len()) {
(max, min) if max > min => (lhs, rhs, max, min),
(min, max) => (rhs, lhs, max, min),
};
let pad = iter::repeat(0).take(max_len - min_len);
let mut a;
let mut b;
let padded: &mut dyn Iterator<Item = u8> = if little_endian {
a = rhs.iter().copied().chain(pad);
&mut a
} else {
b = pad.chain(rhs.iter().copied());
&mut b
};
let bytes: Vec<u8> = lhs
.iter()
.copied()
.zip(padded)
.map(|(lhs, rhs)| f((lhs as i64, rhs as i64)) as u8)
.collect();
Value::binary(bytes, span)
}
(Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => {
Value::error(
ShellError::PipelineMismatch {
exp_input_type: "input, and argument, to be both int or both binary"
.to_string(),
dst_span: rhs.span(),
src_span: span,
},
span,
)
}
// Propagate errors by explicitly matching them before the final case.
(e @ Value::Error { .. }, _) | (_, e @ Value::Error { .. }) => e.clone(),
(other, Value::Int { .. } | Value::Binary { .. }) | (_, other) => Value::error(
ShellError::OnlySupportsThisInputType {
exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(),
dst_span: head,
src_span: other.span(),
},
span,
),
}
}

View File

@ -1,133 +0,0 @@
use super::binary_op;
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct BitsOr;
impl Command for BitsOr {
fn name(&self) -> &str {
"bits or"
}
fn signature(&self) -> Signature {
Signature::build("bits or")
.input_output_types(vec![
(Type::Int, Type::Int),
(Type::Binary, Type::Binary),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
(
Type::List(Box::new(Type::Binary)),
Type::List(Box::new(Type::Binary)),
),
])
.allow_variants_without_examples(true)
.required(
"target",
SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::Int]),
"right-hand side of the operation",
)
.named(
"endian",
SyntaxShape::String,
"byte encode endian, available options: native(default), little, big",
Some('e'),
)
.category(Category::Bits)
}
fn usage(&self) -> &str {
"Performs bitwise or for ints or binary values."
}
fn search_terms(&self) -> Vec<&str> {
vec!["logic or"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let target: Value = call.req(engine_state, stack, 0)?;
let endian = call.get_flag::<Spanned<String>>(engine_state, stack, "endian")?;
let little_endian = if let Some(endian) = endian {
match endian.item.as_str() {
"native" => cfg!(target_endian = "little"),
"little" => true,
"big" => false,
_ => {
return Err(ShellError::TypeMismatch {
err_message: "Endian must be one of native, little, big".to_string(),
span: endian.span,
})
}
}
} else {
cfg!(target_endian = "little")
};
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
input.map(
move |value| binary_op(&value, &target, little_endian, |(l, r)| l | r, head),
engine_state.ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Apply bits or to two numbers",
example: "2 | bits or 6",
result: Some(Value::test_int(6)),
},
Example {
description: "Apply bitwise or to a list of numbers",
example: "[8 3 2] | bits or 2",
result: Some(Value::test_list(vec![
Value::test_int(10),
Value::test_int(3),
Value::test_int(2),
])),
},
Example {
description: "Apply bitwise or to binary data",
example: "0x[88 cc] | bits or 0x[42 32]",
result: Some(Value::test_binary(vec![0xca, 0xfe])),
},
Example {
description:
"Apply bitwise or to binary data of varying lengths with specified endianness",
example: "0x[c0 ff ee] | bits or 0x[ff] --endian big",
result: Some(Value::test_binary(vec![0xc0, 0xff, 0xff])),
},
Example {
description: "Apply bitwise or to input binary data smaller than the operor",
example: "0x[ff] | bits or 0x[12 34 56] --endian little",
result: Some(Value::test_binary(vec![0xff, 0x34, 0x56])),
},
]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(BitsOr {})
}
}

View File

@ -1,197 +0,0 @@
use super::{get_input_num_type, get_number_bytes, InputNumType, NumberBytes};
use itertools::Itertools;
use nu_cmd_base::input_handler::{operate, CmdArgument};
use nu_engine::command_prelude::*;
struct Arguments {
signed: bool,
bits: usize,
number_size: NumberBytes,
}
impl CmdArgument for Arguments {
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
None
}
}
#[derive(Clone)]
pub struct BitsRol;
impl Command for BitsRol {
fn name(&self) -> &str {
"bits rol"
}
fn signature(&self) -> Signature {
Signature::build("bits rol")
.input_output_types(vec![
(Type::Int, Type::Int),
(Type::Binary, Type::Binary),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
(
Type::List(Box::new(Type::Binary)),
Type::List(Box::new(Type::Binary)),
),
])
.allow_variants_without_examples(true)
.required("bits", SyntaxShape::Int, "number of bits to rotate left")
.switch(
"signed",
"always treat input number as a signed number",
Some('s'),
)
.named(
"number-bytes",
SyntaxShape::Int,
"the word size in number of bytes, it can be 1, 2, 4, 8, auto, default value `8`",
Some('n'),
)
.category(Category::Bits)
}
fn usage(&self) -> &str {
"Bitwise rotate left for ints or binary values."
}
fn search_terms(&self) -> Vec<&str> {
vec!["rotate left"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let bits: usize = call.req(engine_state, stack, 0)?;
let signed = call.has_flag(engine_state, stack, "signed")?;
let number_bytes: Option<Spanned<usize>> =
call.get_flag(engine_state, stack, "number-bytes")?;
let number_size = get_number_bytes(number_bytes, head)?;
// This doesn't match explicit nulls
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: head });
}
let args = Arguments {
signed,
number_size,
bits,
};
operate(action, args, input, head, engine_state.ctrlc.clone())
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Rotate left a number with 2 bits",
example: "17 | bits rol 2",
result: Some(Value::test_int(68)),
},
Example {
description: "Rotate left a list of numbers with 2 bits",
example: "[5 3 2] | bits rol 2",
result: Some(Value::list(
vec![Value::test_int(20), Value::test_int(12), Value::test_int(8)],
Span::test_data(),
)),
},
Example {
description: "rotate left binary data",
example: "0x[c0 ff ee] | bits rol 10",
result: Some(Value::binary(vec![0xff, 0xbb, 0x03], Span::test_data())),
},
]
}
}
fn action(input: &Value, args: &Arguments, span: Span) -> Value {
let Arguments {
signed,
number_size,
bits,
} = *args;
match input {
Value::Int { val, .. } => {
use InputNumType::*;
let val = *val;
let bits = bits as u32;
let input_num_type = get_input_num_type(val, signed, number_size);
let int = match input_num_type {
One => (val as u8).rotate_left(bits) as i64,
Two => (val as u16).rotate_left(bits) as i64,
Four => (val as u32).rotate_left(bits) as i64,
Eight => {
let Ok(i) = i64::try_from((val as u64).rotate_left(bits)) else {
return Value::error(
ShellError::GenericError {
error: "result out of range for specified number".into(),
msg: format!(
"rotating left by {bits} is out of range for the value {val}"
),
span: Some(span),
help: None,
inner: vec![],
},
span,
);
};
i
}
SignedOne => (val as i8).rotate_left(bits) as i64,
SignedTwo => (val as i16).rotate_left(bits) as i64,
SignedFour => (val as i32).rotate_left(bits) as i64,
SignedEight => val.rotate_left(bits),
};
Value::int(int, span)
}
Value::Binary { val, .. } => {
let byte_shift = bits / 8;
let bit_rotate = bits % 8;
let mut bytes = val
.iter()
.copied()
.circular_tuple_windows::<(u8, u8)>()
.map(|(lhs, rhs)| (lhs << bit_rotate) | (rhs >> (8 - bit_rotate)))
.collect::<Vec<u8>>();
bytes.rotate_left(byte_shift);
Value::binary(bytes, span)
}
// Propagate errors by explicitly matching them before the final case.
Value::Error { .. } => input.clone(),
other => Value::error(
ShellError::OnlySupportsThisInputType {
exp_input_type: "int or binary".into(),
wrong_type: other.get_type().to_string(),
dst_span: span,
src_span: other.span(),
},
span,
),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(BitsRol {})
}
}

Some files were not shown because too many files have changed in this diff Show More