chore: add nushell
This commit is contained in:
parent
01e40314bd
commit
cef1629f9a
33
nushell/.cargo/config.toml
Normal file
33
nushell/.cargo/config.toml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
# increase the default windows stack size
|
||||||
|
# statically link the CRT so users don't have to install it
|
||||||
|
rustflags = ["-C", "link-args=-stack:10000000", "-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
|
# keeping this but commentting out in case we need them in the future
|
||||||
|
|
||||||
|
# set a 2 gb stack size (0x80000000 = 2147483648 bytes = 2 GB)
|
||||||
|
# [target.x86_64-unknown-linux-gnu]
|
||||||
|
# rustflags = ["-C", "link-args=-Wl,-z stack-size=0x80000000"]
|
||||||
|
|
||||||
|
# set a 2 gb stack size (0x80000000 = 2147483648 bytes = 2 GB)
|
||||||
|
# [target.x86_64-apple-darwin]
|
||||||
|
# rustflags = ["-C", "link-args=-Wl,-stack_size,0x80000000"]
|
||||||
|
|
||||||
|
# How to use mold in linux and mac
|
||||||
|
|
||||||
|
# [target.x86_64-unknown-linux-gnu]
|
||||||
|
# linker = "clang"
|
||||||
|
# rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/mold"]
|
||||||
|
|
||||||
|
# [target.x86_64-apple-darwin]
|
||||||
|
# linker = "clang"
|
||||||
|
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||||
|
|
||||||
|
# [target.aarch64-apple-darwin]
|
||||||
|
# linker = "clang"
|
||||||
|
# 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
nushell/.gitattributes
vendored
Normal file
2
nushell/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Example of a `.gitattributes` file which reclassifies `.nu` files as Nushell:
|
||||||
|
*.nu linguist-language=Nushell
|
5
nushell/.githooks/pre-commit
Executable file
5
nushell/.githooks/pre-commit
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
use ../toolkit.nu fmt
|
||||||
|
|
||||||
|
fmt --check --verbose
|
6
nushell/.githooks/pre-push
Executable file
6
nushell/.githooks/pre-push
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
use ../toolkit.nu [fmt, clippy]
|
||||||
|
|
||||||
|
fmt --check --verbose
|
||||||
|
clippy --verbose
|
1
nushell/.github/AUTO_ISSUE_TEMPLATE/README.md
vendored
Normal file
1
nushell/.github/AUTO_ISSUE_TEMPLATE/README.md
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
This directory is intended for templates to automatically create issues with the [create-an-issue](https://github.com/JasonEtco/create-an-issue) action.
|
16
nushell/.github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
vendored
Normal file
16
nushell/.github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
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: [](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml)
|
50
nushell/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
50
nushell/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: Thank you for your bug report.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: repro
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce
|
||||||
|
description: Steps to reproduce the behavior (including succinct code examples or screenshots of the observed behavior)
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
placeholder: I expected nu to...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: config
|
||||||
|
attributes:
|
||||||
|
label: Configuration
|
||||||
|
description: "Please run `version | transpose key value | to md --pretty` and paste the output to show OS, features, etc."
|
||||||
|
placeholder: |
|
||||||
|
> version | transpose key value | to md --pretty
|
||||||
|
| key | value |
|
||||||
|
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| version | 0.40.0 |
|
||||||
|
| build_os | linux-x86_64 |
|
||||||
|
| rust_version | rustc 1.56.1 |
|
||||||
|
| cargo_version | cargo 1.56.0 |
|
||||||
|
| pkg_version | 0.40.0 |
|
||||||
|
| build_time | 1980-01-01 00:00:00 +00:00 |
|
||||||
|
| build_rust_channel | release |
|
||||||
|
| features | clipboard-cli, ctrlc, dataframe, default, rustyline, term, trash, uuid, which, zip |
|
||||||
|
| installed_plugins | binaryview, chart bar, chart line, fetch, from bson, from sqlite, inc, match, post, ps, query json, s3, selector, start, sys, textview, to bson, to sqlite, tree, xpath |
|
||||||
|
validations:
|
||||||
|
required: true
|
35
nushell/.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
35
nushell/.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: "When you want a new feature for something that doesn't already exist"
|
||||||
|
labels: ["needs-triage", "enhancement"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Related problem
|
||||||
|
description: Thank you for your feature request.
|
||||||
|
placeholder: |
|
||||||
|
A clear and concise description of what the problem is.
|
||||||
|
Example: I am trying to do [...] but [...]
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: desired
|
||||||
|
attributes:
|
||||||
|
label: "Describe the solution you'd like"
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: "Describe alternatives you've considered"
|
||||||
|
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context and details
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
21
nushell/.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
21
nushell/.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Question
|
||||||
|
description: "When you have a question to ask"
|
||||||
|
labels: "question"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Question
|
||||||
|
description: Leave your question here
|
||||||
|
placeholder: |
|
||||||
|
A clear and concise question
|
||||||
|
Example: Is there any equivalent of bash's $CDPATH in Nu?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional context and details
|
||||||
|
description: Add any other context, screenshots or other media that will help us understand your question here, if needed.
|
||||||
|
validations:
|
||||||
|
required: false
|
40
nushell/.github/dependabot.yml
vendored
Normal file
40
nushell/.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
# docs
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
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:
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types: ["version-update:semver-patch"]
|
||||||
|
groups:
|
||||||
|
# Only update polars as a whole as there are many subcrates that need to
|
||||||
|
# be updated at once. We explicitly depend on some of them, so batch their
|
||||||
|
# updates to not take up dependabot PR slots with dysfunctional PRs
|
||||||
|
polars:
|
||||||
|
patterns:
|
||||||
|
- "polars"
|
||||||
|
- "polars-*"
|
||||||
|
# uutils/coreutils also versions all their workspace crates the same at the moment
|
||||||
|
# Most of them have bleeding edge version requirements (some not)
|
||||||
|
# see: https://github.com/uutils/coreutils/blob/main/Cargo.toml
|
||||||
|
uutils:
|
||||||
|
patterns:
|
||||||
|
- "uucore"
|
||||||
|
- "uu_*"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "wednesday"
|
40
nushell/.github/labeler.yml
vendored
Normal file
40
nushell/.github/labeler.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# A bot for automatically labelling pull requests
|
||||||
|
# See https://github.com/actions/labeler
|
||||||
|
|
||||||
|
dataframe:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu_plugin_polars/**
|
||||||
|
|
||||||
|
std-library:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-std/**
|
||||||
|
|
||||||
|
ci:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- .github/workflows/**
|
||||||
|
|
||||||
|
|
||||||
|
LSP:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-lsp/**
|
||||||
|
|
||||||
|
parser:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- crates/nu-parser/**
|
||||||
|
|
||||||
|
pr:plugins:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
# plugins API
|
||||||
|
- crates/nu-plugin/**
|
||||||
|
- crates/nu-plugin-core/**
|
||||||
|
- crates/nu-plugin-engine/**
|
||||||
|
- crates/nu-plugin-protocol/**
|
||||||
|
- crates/nu-plugin-test-support/**
|
||||||
|
# specific plugins (like polars)
|
||||||
|
- crates/nu_plugin_*/**
|
40
nushell/.github/pull_request_template.md
vendored
Normal file
40
nushell/.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!--
|
||||||
|
if this PR closes one or more issues, you can automatically link the PR with
|
||||||
|
them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g.
|
||||||
|
- this PR should close #xxxx
|
||||||
|
- fixes #xxxx
|
||||||
|
|
||||||
|
you can also mention related issues, PRs or discussions!
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Description
|
||||||
|
<!--
|
||||||
|
Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes.
|
||||||
|
|
||||||
|
Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# 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` to check that you're using the standard code style
|
||||||
|
- `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
|
||||||
|
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> from `nushell` you can also use the `toolkit` as follows
|
||||||
|
> ```bash
|
||||||
|
> use toolkit.nu # or use an `env_change` hook to activate it automatically
|
||||||
|
> toolkit check pr
|
||||||
|
> ```
|
||||||
|
-->
|
||||||
|
|
||||||
|
# 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. -->
|
25
nushell/.github/workflows/audit.yml
vendored
Normal file
25
nushell/.github/workflows/audit.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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.7
|
||||||
|
- uses: rustsec/audit-check@v2.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
52
nushell/.github/workflows/beta-test.yml
vendored
Normal file
52
nushell/.github/workflows/beta-test.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
name: Test on Beta Toolchain
|
||||||
|
# This workflow is made to run our tests on the beta toolchain to validate that
|
||||||
|
# the beta toolchain works.
|
||||||
|
# We do not intend to test here that we are working correctly but rather that
|
||||||
|
# the beta toolchain works correctly.
|
||||||
|
# The ci.yml handles our actual testing with our guarantees.
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# If this workflow fails, GitHub notifications will go to the last person
|
||||||
|
# who edited this line.
|
||||||
|
# See: https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/monitoring-workflows/notifications-for-workflow-runs
|
||||||
|
- cron: '0 0 * * *' # Runs daily at midnight UTC
|
||||||
|
|
||||||
|
env:
|
||||||
|
NUSHELL_CARGO_PROFILE: ci
|
||||||
|
NU_LOG_LEVEL: DEBUG
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
# this job is more for testing the beta toolchain and not our tests, so if
|
||||||
|
# this fails but the tests of the regular ci pass, then this is fine
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
platform: [windows-latest, macos-latest, ubuntu-22.04]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- run: rustup update beta
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: cargo +beta test --workspace --profile ci --exclude nu_plugin_*
|
||||||
|
- name: Check for clean repo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "there are changes";
|
||||||
|
git status --porcelain
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "no changes in working directory";
|
||||||
|
fi
|
12
nushell/.github/workflows/check-msrv.nu
vendored
Normal file
12
nushell/.github/workflows/check-msrv.nu
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
}
|
212
nushell/.github/workflows/ci.yml
vendored
Normal file
212
nushell/.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'patch-release-*'
|
||||||
|
|
||||||
|
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:
|
||||||
|
fmt-clippy:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# Pinning to Ubuntu 22.04 because building on newer Ubuntu versions causes linux-gnu
|
||||||
|
# builds to link against a too-new-for-many-Linux-installs glibc version. Consider
|
||||||
|
# revisiting this when 22.04 is closer to EOL (June 2027)
|
||||||
|
#
|
||||||
|
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB,
|
||||||
|
# instead of 14 GB) which is too little for us right now. Revisit when `dfr` commands are
|
||||||
|
# removed and we're only building the `polars` plugin instead
|
||||||
|
platform: [windows-latest, macos-13, ubuntu-22.04]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
|
||||||
|
- name: cargo fmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
# If changing these settings also change toolkit.nu
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --workspace --exclude nu_plugin_* -- $CLIPPY_OPTIONS
|
||||||
|
|
||||||
|
# In tests we don't have to deny unwrap
|
||||||
|
- name: Clippy of tests
|
||||||
|
run: cargo clippy --tests --workspace --exclude nu_plugin_* -- -D warnings
|
||||||
|
|
||||||
|
- name: Clippy of benchmarks
|
||||||
|
run: cargo clippy --benches --workspace --exclude nu_plugin_* -- -D warnings
|
||||||
|
|
||||||
|
tests:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
platform: [windows-latest, macos-latest, ubuntu-22.04]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: cargo test --workspace --profile ci --exclude nu_plugin_*
|
||||||
|
- name: Check for clean repo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "there are changes";
|
||||||
|
git status --porcelain
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "no changes in working directory";
|
||||||
|
fi
|
||||||
|
|
||||||
|
std-lib-and-python-virtualenv:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-22.04, macos-latest, windows-latest]
|
||||||
|
py:
|
||||||
|
- py
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
|
||||||
|
- name: Install Nushell
|
||||||
|
run: cargo install --path . --locked --force
|
||||||
|
|
||||||
|
- 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
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Install virtualenv
|
||||||
|
run: pip install virtualenv
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Test Nushell in virtualenv
|
||||||
|
run: nu scripts/test_virtualenv.nu
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check for clean repo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "there are changes";
|
||||||
|
git status --porcelain
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "no changes in working directory";
|
||||||
|
fi
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
# Using macOS 13 runner because 14 is based on the M1 and has half as much RAM (7 GB,
|
||||||
|
# instead of 14 GB) which is too little for us right now.
|
||||||
|
#
|
||||||
|
# Failure occurring with clippy for rust 1.77.2
|
||||||
|
platform: [windows-latest, macos-13, ubuntu-22.04]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy --package nu_plugin_* -- $CLIPPY_OPTIONS
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: cargo test --profile ci --package nu_plugin_*
|
||||||
|
|
||||||
|
- name: Check for clean repo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "there are changes";
|
||||||
|
git status --porcelain
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "no changes in working directory";
|
||||||
|
fi
|
||||||
|
|
||||||
|
wasm:
|
||||||
|
env:
|
||||||
|
WASM_OPTIONS: --no-default-features --target wasm32-unknown-unknown
|
||||||
|
CLIPPY_CONF_DIR: ${{ github.workspace }}/clippy/wasm/
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- name: Build WASM
|
||||||
|
command: cargo build
|
||||||
|
args:
|
||||||
|
- name: Clippy WASM
|
||||||
|
command: cargo clippy
|
||||||
|
args: -- $CLIPPY_OPTIONS
|
||||||
|
|
||||||
|
name: ${{ matrix.job.name }}
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain and cache
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
|
||||||
|
- name: Add wasm32-unknown-unknown target
|
||||||
|
run: rustup target add wasm32-unknown-unknown
|
||||||
|
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-cmd-base $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-cmd-extra $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-cmd-lang $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-color-config $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-command $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-derive-value $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-engine $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-glob $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-json $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-parser $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-path $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-pretty-hex $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-protocol $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-std $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-system $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-table $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-term-grid $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nu-utils $WASM_OPTIONS ${{ matrix.job.args }}
|
||||||
|
- run: ${{ matrix.job.command }} -p nuon $WASM_OPTIONS ${{ matrix.job.args }}
|
25
nushell/.github/workflows/friendly-config-reminder.yml
vendored
Normal file
25
nushell/.github/workflows/friendly-config-reminder.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: Comment on changes to the config
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
paths:
|
||||||
|
- 'crates/nu-protocol/src/config/**'
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check if there is already a bot comment
|
||||||
|
uses: peter-evans/find-comment@v3
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
comment-author: 'github-actions[bot]'
|
||||||
|
body-includes: Hey, just a bot checking in!
|
||||||
|
- name: Create comment if there is not
|
||||||
|
if: steps.fc.outputs.comment-id == ''
|
||||||
|
uses: peter-evans/create-or-update-comment@v4
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
body: |
|
||||||
|
Hey, just a bot checking in! You edited files related to the configuration.
|
||||||
|
If you changed any of the default values or added a new config option, don't forget to update the [`doc_config.nu`](https://github.com/nushell/nushell/blob/main/crates/nu-utils/src/default_files/doc_config.nu) which documents the options for our users including the defaults provided by the Rust implementation.
|
||||||
|
If you didn't make a change here, you can just ignore me.
|
19
nushell/.github/workflows/labels.yml
vendored
Normal file
19
nushell/.github/workflows/labels.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Automatically labels PRs based on the configuration file
|
||||||
|
# you are probably looking for 👉 `.github/labeler.yml`
|
||||||
|
name: Label PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request_target
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository_owner == 'nushell'
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v5
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
sync-labels: true
|
30
nushell/.github/workflows/milestone.yml
vendored
Normal file
30
nushell/.github/workflows/milestone.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Description:
|
||||||
|
# - Add milestone to a merged PR automatically
|
||||||
|
# - Add milestone to a closed issue that has a merged PR fix (if any)
|
||||||
|
|
||||||
|
name: Milestone Action
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [closed]
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-milestone:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Milestone Update
|
||||||
|
steps:
|
||||||
|
- name: Set Milestone for PR
|
||||||
|
uses: hustcer/milestone-action@main
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Bind milestone to closed issue that has a merged PR fix
|
||||||
|
- name: Set Milestone for Issue
|
||||||
|
uses: hustcer/milestone-action@v2
|
||||||
|
if: github.event.issue.state == 'closed'
|
||||||
|
with:
|
||||||
|
action: bind-issue
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
284
nushell/.github/workflows/nightly-build.yml
vendored
Normal file
284
nushell/.github/workflows/nightly-build.yml
vendored
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
# 5. https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
|
||||||
|
#
|
||||||
|
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
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
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'
|
||||||
|
# Map a step output to a job output
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.vars.outputs.skip }}
|
||||||
|
build_date: ${{ steps.vars.outputs.build_date }}
|
||||||
|
nightly_tag: ${{ steps.vars.outputs.nightly_tag }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
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
|
||||||
|
if: github.repository == 'nushell/nightly'
|
||||||
|
with:
|
||||||
|
version: 0.105.1
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- name: Create Tag and Output Tag Name
|
||||||
|
if: github.repository == 'nushell/nightly'
|
||||||
|
id: vars
|
||||||
|
shell: nu {0}
|
||||||
|
run: |
|
||||||
|
let date = date now | format date %m%d
|
||||||
|
let version = open Cargo.toml | get package.version
|
||||||
|
let sha_short = (git rev-parse --short origin/main | str trim | str substring 0..6)
|
||||||
|
let latest_meta = http get https://api.github.com/repos/nushell/nightly/releases
|
||||||
|
| sort-by -r created_at
|
||||||
|
| where tag_name =~ nightly
|
||||||
|
| get tag_name?.0? | default ''
|
||||||
|
| parse '{version}-nightly.{build}+{hash}'
|
||||||
|
if ($latest_meta.0?.hash? | default '') == $sha_short {
|
||||||
|
print $'(ansi g)Latest nightly build is up-to-date, skip rebuilding.(ansi reset)'
|
||||||
|
$'skip=true(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
let prev_ver = $latest_meta.0?.version? | default '0.0.0'
|
||||||
|
let build = if ($latest_meta | is-empty) or ($version != $prev_ver) { 1 } else {
|
||||||
|
($latest_meta | get build?.0? | default 0 | into int) + 1
|
||||||
|
}
|
||||||
|
let nightly_tag = $'($version)-nightly.($build)+($sha_short)'
|
||||||
|
$'build_date=($date)(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
$'nightly_tag=($nightly_tag)(char nl)' o>> $env.GITHUB_OUTPUT
|
||||||
|
if (git ls-remote --tags origin $nightly_tag | is-empty) {
|
||||||
|
ls **/Cargo.toml | each {|file|
|
||||||
|
open --raw $file.name
|
||||||
|
| str replace --all $'version = "($version)"' $'version = "($version)-nightly.($build)"'
|
||||||
|
| save --force $file.name
|
||||||
|
}
|
||||||
|
# Disable the following two workflows for the automatic committed changes
|
||||||
|
rm .github/workflows/ci.yml
|
||||||
|
rm .github/workflows/audit.yml
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m $'Update version to ($version)-nightly.($build)'
|
||||||
|
git tag -a $nightly_tag -m $'Nightly build from ($sha_short)'
|
||||||
|
git push origin --tags
|
||||||
|
git push origin main -f
|
||||||
|
}
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Nu
|
||||||
|
needs: prepare
|
||||||
|
if: needs.prepare.outputs.skip != 'true'
|
||||||
|
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
|
||||||
|
- aarch64-unknown-linux-musl
|
||||||
|
- armv7-unknown-linux-gnueabihf
|
||||||
|
- armv7-unknown-linux-musleabihf
|
||||||
|
- riscv64gc-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-musl
|
||||||
|
include:
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
- target: aarch64-pc-windows-msvc
|
||||||
|
os: windows-11-arm
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: armv7-unknown-linux-gnueabihf
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: armv7-unknown-linux-musleabihf
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: riscv64gc-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: loongarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: loongarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 6 for Windows
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.0
|
||||||
|
dotnet workload install wix
|
||||||
|
$wixPath = "$env:USERPROFILE\.dotnet\tools"
|
||||||
|
echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
$env:PATH = "$wixPath;$env:PATH"
|
||||||
|
wix --version
|
||||||
|
|
||||||
|
- 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
|
||||||
|
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
rustflags: ''
|
||||||
|
|
||||||
|
- name: Setup Nushell
|
||||||
|
uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.105.1
|
||||||
|
|
||||||
|
- name: Release Nu Binary
|
||||||
|
id: nu
|
||||||
|
run: nu .github/workflows/release-pkg.nu
|
||||||
|
env:
|
||||||
|
OS: ${{ matrix.os }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Create an Issue for Release Failure
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: JasonEtco/create-an-issue@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
update_existing: true
|
||||||
|
search_existing: open
|
||||||
|
filename: .github/AUTO_ISSUE_TEMPLATE/nightly-build-fail.md
|
||||||
|
|
||||||
|
# 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.9
|
||||||
|
if: ${{ startsWith(github.repository, 'nushell/nightly') }}
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
files: |
|
||||||
|
${{ steps.nu.outputs.msi }}
|
||||||
|
${{ steps.nu.outputs.archive }}
|
||||||
|
tag_name: ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
name: ${{ needs.prepare.outputs.build_date }}-${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
sha256sum:
|
||||||
|
needs: [prepare, release]
|
||||||
|
name: Create Sha256sum
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'nushell/nightly'
|
||||||
|
steps:
|
||||||
|
- name: Download Release Archives
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: >-
|
||||||
|
gh release download ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
--repo ${{ github.repository }}
|
||||||
|
--pattern '*'
|
||||||
|
--dir release
|
||||||
|
- name: Create Checksums
|
||||||
|
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
||||||
|
- name: Publish Checksums
|
||||||
|
uses: softprops/action-gh-release@v2.0.9
|
||||||
|
with:
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
files: SHA256SUMS
|
||||||
|
tag_name: ${{ needs.prepare.outputs.nightly_tag }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
name: Cleanup
|
||||||
|
# Should only run in nushell/nightly repo
|
||||||
|
if: github.repository == 'nushell/nightly'
|
||||||
|
needs: [release, sha256sum]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Setup Nushell
|
||||||
|
uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.105.1
|
||||||
|
|
||||||
|
# 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 | slice $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
|
||||||
|
}
|
62
nushell/.github/workflows/release-msi.nu
vendored
Executable file
62
nushell/.github/workflows/release-msi.nu
vendored
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
# Created: 2025/05/21 19:05:20
|
||||||
|
# Description:
|
||||||
|
# A script to build Windows MSI packages for NuShell. Need wix 6.0 to be installed.
|
||||||
|
# The script will download the specified NuShell release, extract it, and create an MSI package.
|
||||||
|
# Can be run locally or in GitHub Actions.
|
||||||
|
# To run this script locally:
|
||||||
|
# load-env { TARGET: 'x86_64-pc-windows-msvc' REF: '0.103.0' GITHUB_REPOSITORY: 'nushell/nushell' }
|
||||||
|
# nu .github/workflows/release-msi.nu
|
||||||
|
|
||||||
|
def build-msi [] {
|
||||||
|
let target = $env.TARGET
|
||||||
|
# We should read the version from the environment variable first
|
||||||
|
# As we may build the MSI package for a specific version not the latest one
|
||||||
|
let version = $env.MSI_VERSION? | default (open Cargo.toml | get package.version)
|
||||||
|
let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' }
|
||||||
|
|
||||||
|
print $'Building msi package for (ansi g)($target)(ansi reset) with version (ansi g)($version)(ansi reset) from tag (ansi g)($env.REF)(ansi reset)...'
|
||||||
|
fetch-nu-pkg
|
||||||
|
# Create extra Windows msi release package if dotnet and wix are available
|
||||||
|
let installed = [dotnet wix] | all { (which $in | length) > 0 }
|
||||||
|
if $installed and (wix --version | split row . | first | into int) >= 6 {
|
||||||
|
|
||||||
|
print $'(char nl)Start creating Windows msi package with the following contents...'
|
||||||
|
cd wix; hr-line
|
||||||
|
cp nu/README.txt .
|
||||||
|
ls -f nu/* | print
|
||||||
|
./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)'
|
||||||
|
glob **/*.msi | print
|
||||||
|
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
||||||
|
let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/')
|
||||||
|
let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi'
|
||||||
|
mv $wixRelease $msi
|
||||||
|
print $'MSI archive: ---> ($msi)';
|
||||||
|
# Run only in GitHub Actions
|
||||||
|
if ($env.GITHUB_ACTIONS? | default false | into bool) {
|
||||||
|
echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch-nu-pkg [] {
|
||||||
|
mkdir wix/nu
|
||||||
|
# See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables
|
||||||
|
gh release download $env.REF --repo $env.GITHUB_REPOSITORY --pattern $'*-($env.TARGET).zip' --dir wix/nu
|
||||||
|
cd wix/nu
|
||||||
|
let pkg = ls *.zip | get name.0
|
||||||
|
unzip $pkg
|
||||||
|
rm $pkg
|
||||||
|
ls | print
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print a horizontal line marker
|
||||||
|
def 'hr-line' [
|
||||||
|
--blank-line(-b)
|
||||||
|
] {
|
||||||
|
print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)'
|
||||||
|
if $blank_line { char nl }
|
||||||
|
}
|
||||||
|
|
||||||
|
alias main = build-msi
|
103
nushell/.github/workflows/release-msi.yml
vendored
Normal file
103
nushell/.github/workflows/release-msi.yml
vendored
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
#
|
||||||
|
# REF:
|
||||||
|
# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude
|
||||||
|
#
|
||||||
|
name: Build Windows MSI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
description: 'Tag to Rebuild MSI'
|
||||||
|
version:
|
||||||
|
description: 'Version of Rebuild MSI'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Nu
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- x86_64-pc-windows-msvc
|
||||||
|
- aarch64-pc-windows-msvc
|
||||||
|
extra: ['bin']
|
||||||
|
|
||||||
|
include:
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
- target: aarch64-pc-windows-msvc
|
||||||
|
os: windows-11-arm
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 6 for Windows
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.0
|
||||||
|
dotnet workload install wix
|
||||||
|
$wixPath = "$env:USERPROFILE\.dotnet\tools"
|
||||||
|
echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
$env:PATH = "$wixPath;$env:PATH"
|
||||||
|
wix --version
|
||||||
|
|
||||||
|
- name: Setup Nushell
|
||||||
|
uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.105.1
|
||||||
|
|
||||||
|
- name: Release MSI Packages
|
||||||
|
id: nu
|
||||||
|
run: nu .github/workflows/release-msi.nu
|
||||||
|
env:
|
||||||
|
OS: ${{ matrix.os }}
|
||||||
|
REF: ${{ inputs.tag }}
|
||||||
|
TARGET: ${{ matrix.target }}
|
||||||
|
MSI_VERSION: ${{ inputs.version }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# REF: https://github.com/marketplace/actions/gh-release
|
||||||
|
- name: Publish Archive
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
with:
|
||||||
|
tag_name: ${{ inputs.tag }}
|
||||||
|
files: ${{ steps.nu.outputs.msi }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
sha256sum:
|
||||||
|
needs: release
|
||||||
|
name: Create Sha256sum
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download Release Archives
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: >-
|
||||||
|
gh release download ${{ inputs.tag }}
|
||||||
|
--repo ${{ github.repository }}
|
||||||
|
--pattern '*'
|
||||||
|
--dir release
|
||||||
|
- name: Create Checksums
|
||||||
|
run: cd release && rm -f SHA256SUMS && shasum -a 256 * > ../SHA256SUMS
|
||||||
|
- name: Publish Checksums
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
with:
|
||||||
|
files: SHA256SUMS
|
||||||
|
tag_name: ${{ inputs.tag }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
254
nushell/.github/workflows/release-pkg.nu
vendored
Executable file
254
nushell/.github/workflows/release-pkg.nu
vendored
Executable file
@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env nu
|
||||||
|
|
||||||
|
# Created: 2022/05/26 19:05:20
|
||||||
|
# Description:
|
||||||
|
# A script to do the github release task, need nushell to be installed.
|
||||||
|
# REF:
|
||||||
|
# 1. https://github.com/volks73/cargo-wix
|
||||||
|
|
||||||
|
# Instructions for manually creating an MSI for Winget Releases when they fail
|
||||||
|
# Added 2022-11-29 when Windows packaging wouldn't work
|
||||||
|
# Updated again on 2023-02-23 because MSIs are still failing validation
|
||||||
|
# To run this manual for windows here are the steps I take
|
||||||
|
# checkout the release you want to publish
|
||||||
|
# 1. git checkout 0.103.0
|
||||||
|
# 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. hide-env CARGO_TARGET_DIR
|
||||||
|
# 3. $env.TARGET = 'x86_64-pc-windows-msvc'
|
||||||
|
# 4. $env.GITHUB_WORKSPACE = 'D:\nushell'
|
||||||
|
# 5. $env.GITHUB_OUTPUT = 'D:\nushell\output\out.txt'
|
||||||
|
# 6. $env.OS = 'windows-latest'
|
||||||
|
# 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')
|
||||||
|
# make sure aria2c.exe is in your path https://github.com/aria2/aria2
|
||||||
|
# 8. $env.Path = ($env.Path | append 'c:\path\to\aria2c')
|
||||||
|
# make sure you have the wix 6.0 installed: dotnet tool install --global wix --version 6.0.0
|
||||||
|
# then build nu*.exe and the MSI installer by running:
|
||||||
|
# 9. source .github\workflows\release-pkg.nu
|
||||||
|
# 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
|
||||||
|
# on the winget-pkgs PR. To generate the hash, run this command
|
||||||
|
# 10. open wix\bin\x64\Release\nu-0.103.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
|
||||||
|
|
||||||
|
|
||||||
|
# The main binary file to be released
|
||||||
|
let bin = 'nu'
|
||||||
|
let os = $env.OS
|
||||||
|
let target = $env.TARGET
|
||||||
|
# Repo source dir like `/home/runner/work/nushell/nushell`
|
||||||
|
let src = $env.GITHUB_WORKSPACE
|
||||||
|
let dist = $'($env.GITHUB_WORKSPACE)/output'
|
||||||
|
let version = (open Cargo.toml | get package.version)
|
||||||
|
|
||||||
|
print $'Debugging info:'
|
||||||
|
print { version: $version, bin: $bin, os: $os, target: $target, src: $src, dist: $dist }; hr-line -b
|
||||||
|
|
||||||
|
# $env
|
||||||
|
|
||||||
|
let USE_UBUNTU = $os starts-with ubuntu
|
||||||
|
|
||||||
|
print $'(char nl)Packaging ($bin) v($version) for ($target) in ($src)...'; hr-line -b
|
||||||
|
if not ('Cargo.lock' | path exists) { cargo generate-lockfile }
|
||||||
|
|
||||||
|
print $'Start building ($bin)...'; hr-line
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Build for Ubuntu and macOS
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if $os in ['macos-latest'] or $USE_UBUNTU {
|
||||||
|
if $USE_UBUNTU {
|
||||||
|
sudo apt update
|
||||||
|
sudo apt-get install libxcb-composite0-dev -y
|
||||||
|
}
|
||||||
|
match $target {
|
||||||
|
'aarch64-unknown-linux-gnu' => {
|
||||||
|
sudo apt-get install gcc-aarch64-linux-gnu -y
|
||||||
|
$env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER = 'aarch64-linux-gnu-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'riscv64gc-unknown-linux-gnu' => {
|
||||||
|
sudo apt-get install gcc-riscv64-linux-gnu -y
|
||||||
|
$env.CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER = 'riscv64-linux-gnu-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'armv7-unknown-linux-gnueabihf' => {
|
||||||
|
sudo apt-get install pkg-config gcc-arm-linux-gnueabihf -y
|
||||||
|
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER = 'arm-linux-gnueabihf-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'aarch64-unknown-linux-musl' => {
|
||||||
|
aria2c https://github.com/nushell/integrations/releases/download/build-tools/aarch64-linux-musl-cross.tgz
|
||||||
|
tar -xf aarch64-linux-musl-cross.tgz -C $env.HOME
|
||||||
|
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.HOME)/aarch64-linux-musl-cross/bin')
|
||||||
|
$env.CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER = 'aarch64-linux-musl-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'armv7-unknown-linux-musleabihf' => {
|
||||||
|
aria2c https://github.com/nushell/integrations/releases/download/build-tools/armv7r-linux-musleabihf-cross.tgz
|
||||||
|
tar -xf armv7r-linux-musleabihf-cross.tgz -C $env.HOME
|
||||||
|
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.HOME)/armv7r-linux-musleabihf-cross/bin')
|
||||||
|
$env.CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER = 'armv7r-linux-musleabihf-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'loongarch64-unknown-linux-gnu' => {
|
||||||
|
aria2c https://github.com/loongson/build-tools/releases/download/2024.08.08/x86_64-cross-tools-loongarch64-binutils_2.43-gcc_14.2.0-glibc_2.40.tar.xz
|
||||||
|
tar xf x86_64-cross-tools-loongarch64-*.tar.xz
|
||||||
|
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.PWD)/cross-tools/bin')
|
||||||
|
$env.CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER = 'loongarch64-unknown-linux-gnu-gcc'
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
'loongarch64-unknown-linux-musl' => {
|
||||||
|
print $"(ansi g)Downloading LoongArch64 musl cross-compilation toolchain...(ansi reset)"
|
||||||
|
aria2c -q https://github.com/LoongsonLab/oscomp-toolchains-for-oskernel/releases/download/loongarch64-linux-musl-cross-gcc-13.2.0/loongarch64-linux-musl-cross.tgz
|
||||||
|
tar -xf loongarch64-linux-musl-cross.tgz
|
||||||
|
$env.PATH = ($env.PATH | split row (char esep) | prepend $'($env.PWD)/loongarch64-linux-musl-cross/bin')
|
||||||
|
$env.CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_MUSL_LINKER = "loongarch64-linux-musl-gcc"
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Build for Windows without static-link-openssl feature
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
if $os =~ 'windows' {
|
||||||
|
cargo-build-nu
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Prepare for the release archive
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
let suffix = if $os =~ 'windows' { '.exe' }
|
||||||
|
# nu, nu_plugin_* were all included
|
||||||
|
let executable = $'target/($target)/release/($bin)*($suffix)'
|
||||||
|
print $'Current executable file: ($executable)'
|
||||||
|
|
||||||
|
cd $src; mkdir $dist;
|
||||||
|
rm -rf ...(glob $'target/($target)/release/*.d') ...(glob $'target/($target)/release/nu_pretty_hex*')
|
||||||
|
print $'(char nl)All executable files:'; hr-line
|
||||||
|
# We have to use `print` here to make sure the command output is displayed
|
||||||
|
print (ls -f ($executable | into glob)); sleep 1sec
|
||||||
|
|
||||||
|
print $'(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.
|
||||||
|
Then you can use `plugin use` to load the plugin into your session.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
> plugin add ./nu_plugin_query
|
||||||
|
> 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' {
|
||||||
|
(do -i { .\output\nu.exe -c 'version' }) | default '' | str join
|
||||||
|
} else {
|
||||||
|
(do -i { ./output/nu -c 'version' }) | default '' | str join
|
||||||
|
}
|
||||||
|
if ($ver | str trim | is-empty) {
|
||||||
|
print $'(ansi r)Incompatible Nu binary: The binary cross compiled is not runnable on current arch...(ansi reset)'
|
||||||
|
} else { print $ver }
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Create a release archive and send it to output for the following steps
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
cd $dist; print $'(char nl)Creating release archive...'; hr-line
|
||||||
|
if $os in ['macos-latest'] or $USE_UBUNTU {
|
||||||
|
|
||||||
|
let files = (ls | get name)
|
||||||
|
let dest = $'($bin)-($version)-($target)'
|
||||||
|
let archive = $'($dist)/($dest).tar.gz'
|
||||||
|
|
||||||
|
mkdir $dest
|
||||||
|
$files | each {|it| cp -v $it $dest }
|
||||||
|
|
||||||
|
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls $dest | print
|
||||||
|
|
||||||
|
tar -czf $archive $dest
|
||||||
|
print $'archive: ---> ($archive)'; ls $archive
|
||||||
|
# REF: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
|
||||||
|
echo $"archive=($archive)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
|
||||||
|
} else if $os =~ 'windows' {
|
||||||
|
|
||||||
|
let releaseStem = $'($bin)-($version)-($target)'
|
||||||
|
let arch = if $nu.os-info.arch =~ 'x86_64' { 'x64' } else { 'arm64' }
|
||||||
|
fetch-less $arch
|
||||||
|
|
||||||
|
print $'(char nl)(ansi g)Archive contents:(ansi reset)'; hr-line; ls | print
|
||||||
|
let archive = $'($dist)/($releaseStem).zip'
|
||||||
|
7z a $archive ...(glob *)
|
||||||
|
let pkg = (ls -f $archive | get name)
|
||||||
|
if not ($pkg | is-empty) {
|
||||||
|
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
||||||
|
let archive = ($pkg | get 0 | str replace --all '\' '/')
|
||||||
|
print $'archive: ---> ($archive)'
|
||||||
|
echo $"archive=($archive)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create extra Windows msi release package if dotnet and wix are available
|
||||||
|
let installed = [dotnet wix] | all { (which $in | length) > 0 }
|
||||||
|
if $installed and (wix --version | split row . | first | into int) >= 6 {
|
||||||
|
|
||||||
|
print $'(char nl)Start creating Windows msi package with the following contents...'
|
||||||
|
cd $src; cd wix; hr-line; mkdir nu
|
||||||
|
# Wix need the binaries be stored in nu folder
|
||||||
|
cp -r ($'($dist)/*' | into glob) nu/
|
||||||
|
cp $'($dist)/README.txt' .
|
||||||
|
ls -f nu/* | print
|
||||||
|
./nu/nu.exe -c $'NU_RELEASE_VERSION=($version) dotnet build -c Release -p:Platform=($arch)'
|
||||||
|
glob **/*.msi | print
|
||||||
|
# Workaround for https://github.com/softprops/action-gh-release/issues/280
|
||||||
|
let wixRelease = (glob **/*.msi | where $it =~ bin | get 0 | str replace --all '\' '/')
|
||||||
|
let msi = $'($wixRelease | path dirname)/nu-($version)-($target).msi'
|
||||||
|
mv $wixRelease $msi
|
||||||
|
print $'MSI archive: ---> ($msi)';
|
||||||
|
echo $"msi=($msi)(char nl)" o>> $env.GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch-less [
|
||||||
|
arch: string = 'x64' # The architecture to fetch
|
||||||
|
] {
|
||||||
|
let less_zip = $'less-($arch).zip'
|
||||||
|
print $'Fetching less archive: (ansi g)($less_zip)(ansi reset)'
|
||||||
|
let url = $'https://github.com/jftuga/less-Windows/releases/download/less-v668/($less_zip)'
|
||||||
|
http get https://github.com/jftuga/less-Windows/blob/master/LICENSE | save -rf LICENSE-for-less.txt
|
||||||
|
http get $url | save -rf $less_zip
|
||||||
|
unzip $less_zip
|
||||||
|
rm $less_zip lesskey.exe
|
||||||
|
}
|
||||||
|
|
||||||
|
def 'cargo-build-nu' [] {
|
||||||
|
if $os =~ 'windows' {
|
||||||
|
cargo build --release --all --target $target
|
||||||
|
} else {
|
||||||
|
cargo build --release --all --target $target --features=static-link-openssl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print a horizontal line marker
|
||||||
|
def 'hr-line' [
|
||||||
|
--blank-line(-b)
|
||||||
|
] {
|
||||||
|
print $'(ansi g)---------------------------------------------------------------------------->(ansi reset)'
|
||||||
|
if $blank_line { char nl }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the specified env key's value or ''
|
||||||
|
def 'get-env' [
|
||||||
|
key: string # The key to get it's env value
|
||||||
|
default: string = '' # The default value for an empty env
|
||||||
|
] {
|
||||||
|
$env | get -i $key | default $default
|
||||||
|
}
|
141
nushell/.github/workflows/release.yml
vendored
Normal file
141
nushell/.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#
|
||||||
|
# REF:
|
||||||
|
# 1. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixinclude
|
||||||
|
#
|
||||||
|
name: Create Release Draft
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||||
|
- '!*nightly*' # Don't trigger release for nightly tags
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Nu
|
||||||
|
|
||||||
|
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
|
||||||
|
- aarch64-unknown-linux-musl
|
||||||
|
- armv7-unknown-linux-gnueabihf
|
||||||
|
- armv7-unknown-linux-musleabihf
|
||||||
|
- riscv64gc-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-gnu
|
||||||
|
- loongarch64-unknown-linux-musl
|
||||||
|
include:
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
- target: aarch64-pc-windows-msvc
|
||||||
|
os: windows-11-arm
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: armv7-unknown-linux-gnueabihf
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: armv7-unknown-linux-musleabihf
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: riscv64gc-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: loongarch64-unknown-linux-gnu
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- target: loongarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-22.04
|
||||||
|
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Wix Toolset 6 for Windows
|
||||||
|
shell: pwsh
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||||
|
run: |
|
||||||
|
dotnet tool install --global wix --version 6.0.0
|
||||||
|
dotnet workload install wix
|
||||||
|
$wixPath = "$env:USERPROFILE\.dotnet\tools"
|
||||||
|
echo "$wixPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
$env:PATH = "$wixPath;$env:PATH"
|
||||||
|
wix --version
|
||||||
|
|
||||||
|
- name: Update Rust Toolchain Target
|
||||||
|
run: |
|
||||||
|
echo "targets = ['${{matrix.target}}']" >> rust-toolchain.toml
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1.12.0
|
||||||
|
# WARN: Keep the rustflags to prevent from the winget submission error: `CAQuietExec: Error 0xc0000135`
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
rustflags: ''
|
||||||
|
|
||||||
|
- name: Setup Nushell
|
||||||
|
uses: hustcer/setup-nu@v3
|
||||||
|
with:
|
||||||
|
version: 0.105.1
|
||||||
|
|
||||||
|
- name: Release Nu Binary
|
||||||
|
id: nu
|
||||||
|
run: nu .github/workflows/release-pkg.nu
|
||||||
|
env:
|
||||||
|
OS: ${{ matrix.os }}
|
||||||
|
REF: ${{ github.ref }}
|
||||||
|
TARGET: ${{ matrix.target }}
|
||||||
|
|
||||||
|
# WARN: Don't upgrade this action due to the release per asset issue.
|
||||||
|
# See: https://github.com/softprops/action-gh-release/issues/445
|
||||||
|
- name: Publish Archive
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
files: |
|
||||||
|
${{ steps.nu.outputs.msi }}
|
||||||
|
${{ steps.nu.outputs.archive }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
sha256sum:
|
||||||
|
needs: release
|
||||||
|
name: Create Sha256sum
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download Release Archives
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: >-
|
||||||
|
gh release download ${{ github.ref_name }}
|
||||||
|
--repo ${{ github.repository }}
|
||||||
|
--pattern '*'
|
||||||
|
--dir release
|
||||||
|
- name: Create Checksums
|
||||||
|
run: cd release && shasum -a 256 * > ../SHA256SUMS
|
||||||
|
- name: Publish Checksums
|
||||||
|
uses: softprops/action-gh-release@v2.0.5
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
files: SHA256SUMS
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
13
nushell/.github/workflows/typos.yml
vendored
Normal file
13
nushell/.github/workflows/typos.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
name: Typos
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
name: Spell Check with Typos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Actions Repository
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Check spelling
|
||||||
|
uses: crate-ci/typos@v1.33.1
|
34
nushell/.github/workflows/winget-submission.yml
vendored
Normal file
34
nushell/.github/workflows/winget-submission.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Submit Nushell package to Windows Package Manager Community Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag_name:
|
||||||
|
description: 'Specific tag name'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
winget:
|
||||||
|
name: Publish winget package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Submit package to Windows Package Manager Community Repository
|
||||||
|
uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
|
with:
|
||||||
|
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 }}
|
||||||
|
release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
|
||||||
|
token: ${{ secrets.NUSHELL_PAT }}
|
||||||
|
fork-user: nushell
|
55
nushell/.gitignore
vendored
Normal file
55
nushell/.gitignore
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/target
|
||||||
|
/scratch
|
||||||
|
**/*.rs.bk
|
||||||
|
history.txt
|
||||||
|
tests/fixtures/nuplayground
|
||||||
|
crates/*/target
|
||||||
|
.mailmap
|
||||||
|
|
||||||
|
# Debian/Ubuntu
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/files
|
||||||
|
debian/nu.substvars
|
||||||
|
debian/nu/
|
||||||
|
|
||||||
|
# macOS junk
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# JetBrains' IDE items
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
# VSCode's IDE items
|
||||||
|
.vscode/*
|
||||||
|
|
||||||
|
# JetBrains' Fleet IDE
|
||||||
|
.fleet/*
|
||||||
|
|
||||||
|
# Visual Studio Extension SourceGear Rust items
|
||||||
|
VSWorkspaceSettings.json
|
||||||
|
unstable_cargo_features.txt
|
||||||
|
|
||||||
|
# Helix configuration folder
|
||||||
|
.helix/*
|
||||||
|
.helix
|
||||||
|
wix/bin/
|
||||||
|
wix/obj/
|
||||||
|
wix/nu/
|
||||||
|
|
||||||
|
# Coverage tools
|
||||||
|
lcov.info
|
||||||
|
tarpaulin-report.html
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/*
|
||||||
|
*.rsproj
|
||||||
|
*.rsproj.user
|
||||||
|
*.sln
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv/
|
||||||
|
.envrc
|
||||||
|
|
||||||
|
# pre-commit-hooks
|
||||||
|
.pre-commit-config.yaml
|
26
nushell/CITATION.cff
Normal file
26
nushell/CITATION.cff
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
title: 'Nushell'
|
||||||
|
message: >-
|
||||||
|
If you use this software and wish to cite it,
|
||||||
|
you can use the metadata from this file.
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- name: "The Nushell Project Team"
|
||||||
|
identifiers:
|
||||||
|
- type: url
|
||||||
|
value: 'https://github.com/nushell/nushell'
|
||||||
|
description: Repository
|
||||||
|
repository-code: 'https://github.com/nushell/nushell'
|
||||||
|
url: 'https://www.nushell.sh/'
|
||||||
|
abstract: >-
|
||||||
|
The goal of the Nushell project is to take the Unix
|
||||||
|
philosophy of shells, where pipes connect simple commands
|
||||||
|
together, and bring it to the modern style of development.
|
||||||
|
Thus, rather than being either a shell, or a programming
|
||||||
|
language, Nushell connects both by bringing a rich
|
||||||
|
programming language and a full-featured shell together
|
||||||
|
into one package.
|
||||||
|
keywords:
|
||||||
|
- nushell
|
||||||
|
- shell
|
||||||
|
license: MIT
|
76
nushell/CODE_OF_CONDUCT.md
Normal file
76
nushell/CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to make participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all project spaces, and it also applies when
|
||||||
|
an individual is representing the project or its community in public spaces.
|
||||||
|
Examples of representing a project or community include using an official
|
||||||
|
project e-mail address, posting via an official social media account, or acting
|
||||||
|
as an appointed representative at an online or offline event. Representation of
|
||||||
|
a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at wycats@gmail.com via email or by reaching out to @jturner, @gedge, or @andras_io on discord. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
<https://www.contributor-covenant.org/faq>
|
236
nushell/CONTRIBUTING.md
Normal file
236
nushell/CONTRIBUTING.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Welcome to Nushell and thank you for considering contributing!
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
- [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.
|
||||||
|
This saves both your and our time if we realize the change needs to go another direction before spending time on it.
|
||||||
|
So, please, reach out and tell us what you want to do.
|
||||||
|
This will significantly increase the chance of your PR being accepted.
|
||||||
|
|
||||||
|
The review process can be summarized as follows:
|
||||||
|
1. You want to make some change to Nushell that is more involved than simple bug-fixing.
|
||||||
|
2. Go to [Discord](https://discordapp.com/invite/NtAbbGn) or a [GitHub issue](https://github.com/nushell/nushell/issues/new/choose) and chat with some core team members and/or other contributors about it.
|
||||||
|
3. After getting a green light from the core team, implement the feature, open a pull request (PR) and write a concise but comprehensive description of the change.
|
||||||
|
4. If your PR includes any user-facing features (such as adding a flag to a command), clearly list them in the PR description.
|
||||||
|
5. Then, core team members and other regular contributors will review the PR and suggest changes.
|
||||||
|
6. When we all agree, the PR will be merged.
|
||||||
|
7. If your PR includes any user-facing features, make sure the changes are also reflected in [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged.
|
||||||
|
8. Congratulate yourself, you just improved Nushell! :-)
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Nushell requires a recent Rust toolchain and some dependencies; [refer to the Nu Book for up-to-date requirements](https://www.nushell.sh/book/installation.html#build-from-source). After installing dependencies, you should be able to clone+build Nu like any other Rust project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nushell/nushell
|
||||||
|
cd nushell
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Tests can be found in different places:
|
||||||
|
* `/tests`
|
||||||
|
* command examples
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run Clippy on Nushell:
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo clippy --workspace -- -D warnings -D clippy::unwrap_used
|
||||||
|
```
|
||||||
|
or via the `toolkit.nu` command:
|
||||||
|
```nushell
|
||||||
|
use toolkit.nu clippy
|
||||||
|
clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run all tests:
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
or via the `toolkit.nu` command:
|
||||||
|
```nushell
|
||||||
|
use toolkit.nu test
|
||||||
|
test
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run all tests for a specific command
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo test --package nu-cli --test main -- commands::<command_name_here>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check to see if there are code formatting issues
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
```
|
||||||
|
or via the `toolkit.nu` command:
|
||||||
|
```nushell
|
||||||
|
use toolkit.nu fmt
|
||||||
|
fmt --check
|
||||||
|
```
|
||||||
|
|
||||||
|
- Format the code in the project
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
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
|
||||||
|
|
||||||
|
- To view verbose logs when developing, enable the `trace` log level.
|
||||||
|
|
||||||
|
```nushell
|
||||||
|
cargo run --release -- --log-level trace
|
||||||
|
```
|
||||||
|
|
||||||
|
- To redirect trace logs to a file, enable the `--log-target file` switch.
|
||||||
|
```nushell
|
||||||
|
cargo run --release -- --log-level trace --log-target file
|
||||||
|
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.
|
8867
nushell/Cargo.lock
generated
Normal file
8867
nushell/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
344
nushell/Cargo.toml
Normal file
344
nushell/Cargo.toml
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["The Nushell Project Developers"]
|
||||||
|
build = "scripts/build.rs"
|
||||||
|
default-run = "nu"
|
||||||
|
description = "A new type of shell"
|
||||||
|
documentation = "https://www.nushell.sh/book/"
|
||||||
|
edition = "2024"
|
||||||
|
exclude = ["images"]
|
||||||
|
homepage = "https://www.nushell.sh"
|
||||||
|
license = "MIT"
|
||||||
|
name = "nu"
|
||||||
|
repository = "https://github.com/nushell/nushell"
|
||||||
|
#rust-version = "1.85.1"
|
||||||
|
version = "0.105.2"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[package.metadata.binstall]
|
||||||
|
pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ version }-{ target }.{ archive-format }"
|
||||||
|
pkg-fmt = "tgz"
|
||||||
|
|
||||||
|
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||||
|
pkg-fmt = "zip"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"crates/nu-cli",
|
||||||
|
"crates/nu-engine",
|
||||||
|
"crates/nu-parser",
|
||||||
|
"crates/nu-system",
|
||||||
|
"crates/nu-cmd-base",
|
||||||
|
"crates/nu-cmd-extra",
|
||||||
|
"crates/nu-cmd-lang",
|
||||||
|
"crates/nu-cmd-plugin",
|
||||||
|
"crates/nu-command",
|
||||||
|
"crates/nu-color-config",
|
||||||
|
"crates/nu-explore",
|
||||||
|
"crates/nu-json",
|
||||||
|
"crates/nu-lsp",
|
||||||
|
"crates/nu-pretty-hex",
|
||||||
|
"crates/nu-protocol",
|
||||||
|
"crates/nu-derive-value",
|
||||||
|
"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.9"
|
||||||
|
anyhow = "1.0.82"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
bracoxide = "0.1.6"
|
||||||
|
brotli = "7.0"
|
||||||
|
byteorder = "1.5"
|
||||||
|
bytes = "1"
|
||||||
|
bytesize = "1.3.3"
|
||||||
|
calamine = "0.28"
|
||||||
|
chardetng = "0.1.17"
|
||||||
|
chrono = { default-features = false, version = "0.4.34" }
|
||||||
|
chrono-humanize = "0.2.3"
|
||||||
|
chrono-tz = "0.10"
|
||||||
|
crossbeam-channel = "0.5.8"
|
||||||
|
crossterm = "0.28.1"
|
||||||
|
csv = "1.3"
|
||||||
|
ctrlc = "3.4"
|
||||||
|
devicons = "0.6.12"
|
||||||
|
dialoguer = { default-features = false, version = "0.11" }
|
||||||
|
digest = { default-features = false, version = "0.10" }
|
||||||
|
dirs = "5.0"
|
||||||
|
dirs-sys = "0.4"
|
||||||
|
dtparse = "2.0"
|
||||||
|
encoding_rs = "0.8"
|
||||||
|
fancy-regex = "0.14"
|
||||||
|
filesize = "0.2"
|
||||||
|
filetime = "0.2"
|
||||||
|
heck = "0.5.0"
|
||||||
|
human-date-parser = "0.3.0"
|
||||||
|
indexmap = "2.9"
|
||||||
|
indicatif = "0.17"
|
||||||
|
interprocess = "2.2.0"
|
||||||
|
is_executable = "1.0"
|
||||||
|
itertools = "0.14"
|
||||||
|
libc = "0.2"
|
||||||
|
libproc = "0.14"
|
||||||
|
log = "0.4"
|
||||||
|
lru = "0.12"
|
||||||
|
lscolors = { version = "0.20", default-features = false }
|
||||||
|
lsp-server = "0.7.8"
|
||||||
|
lsp-types = { version = "0.97.0", features = ["proposed"] }
|
||||||
|
lsp-textdocument = "0.4.2"
|
||||||
|
mach2 = "0.4"
|
||||||
|
md5 = { version = "0.10", package = "md-5" }
|
||||||
|
miette = "7.6"
|
||||||
|
mime = "0.3.17"
|
||||||
|
mime_guess = "2.0"
|
||||||
|
mockito = { version = "1.7", default-features = false }
|
||||||
|
multipart-rs = "0.1.13"
|
||||||
|
native-tls = "0.2"
|
||||||
|
nix = { version = "0.29", default-features = false }
|
||||||
|
notify-debouncer-full = { version = "0.3", default-features = false }
|
||||||
|
nu-ansi-term = "0.50.1"
|
||||||
|
nucleo-matcher = "0.3"
|
||||||
|
num-format = "0.4"
|
||||||
|
num-traits = "0.2"
|
||||||
|
oem_cp = "2.0.0"
|
||||||
|
omnipath = "0.1"
|
||||||
|
open = "5.3"
|
||||||
|
os_pipe = { version = "1.2", features = ["io_safety"] }
|
||||||
|
pathdiff = "0.2"
|
||||||
|
percent-encoding = "2"
|
||||||
|
pretty_assertions = "1.4"
|
||||||
|
print-positions = "0.6"
|
||||||
|
proc-macro-error2 = "2.0"
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
procfs = "0.17.0"
|
||||||
|
pwd = "1.3"
|
||||||
|
quick-xml = "0.37.0"
|
||||||
|
quickcheck = "1.0"
|
||||||
|
quickcheck_macros = "1.1"
|
||||||
|
quote = "1.0"
|
||||||
|
rand = "0.9"
|
||||||
|
getrandom = "0.2" # pick same version that rand requires
|
||||||
|
rand_chacha = "0.9"
|
||||||
|
ratatui = "0.29"
|
||||||
|
rayon = "1.10"
|
||||||
|
reedline = "0.40.0"
|
||||||
|
rmp = "0.8"
|
||||||
|
rmp-serde = "1.3"
|
||||||
|
roxmltree = "0.20"
|
||||||
|
rstest = { version = "0.23", default-features = false }
|
||||||
|
rstest_reuse = "0.7"
|
||||||
|
rusqlite = "0.31"
|
||||||
|
rust-embed = "8.7.0"
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["std", "tls12"] }
|
||||||
|
rustls-native-certs = "0.8"
|
||||||
|
scopeguard = { version = "1.2.0" }
|
||||||
|
serde = { version = "1.0" }
|
||||||
|
serde_json = "1.0.97"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
|
serde_yaml = "0.9.33"
|
||||||
|
sha2 = "0.10"
|
||||||
|
strip-ansi-escapes = "0.2.0"
|
||||||
|
strum = "0.26"
|
||||||
|
strum_macros = "0.26"
|
||||||
|
syn = "2.0"
|
||||||
|
sysinfo = "0.33"
|
||||||
|
tabled = { version = "0.20", default-features = false }
|
||||||
|
tempfile = "3.20"
|
||||||
|
titlecase = "3.6"
|
||||||
|
toml = "0.8"
|
||||||
|
trash = "5.2"
|
||||||
|
update-informer = { version = "1.2.0", default-features = false, features = ["github", "ureq"] }
|
||||||
|
umask = "2.1"
|
||||||
|
unicode-segmentation = "1.12"
|
||||||
|
unicode-width = "0.2"
|
||||||
|
ureq = { version = "2.12", default-features = false, features = ["socks-proxy"] }
|
||||||
|
url = "2.2"
|
||||||
|
uu_cp = "0.0.30"
|
||||||
|
uu_mkdir = "0.0.30"
|
||||||
|
uu_mktemp = "0.0.30"
|
||||||
|
uu_mv = "0.0.30"
|
||||||
|
uu_touch = "0.0.30"
|
||||||
|
uu_whoami = "0.0.30"
|
||||||
|
uu_uname = "0.0.30"
|
||||||
|
uucore = "0.0.30"
|
||||||
|
uuid = "1.16.0"
|
||||||
|
v_htmlescape = "0.15.0"
|
||||||
|
wax = "0.6"
|
||||||
|
web-time = "1.1.0"
|
||||||
|
which = "7.0.3"
|
||||||
|
windows = "0.56"
|
||||||
|
windows-sys = "0.48"
|
||||||
|
winreg = "0.52"
|
||||||
|
memchr = "2.7.4"
|
||||||
|
webpki-roots = "1.0"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
# Warning: workspace lints affect library code as well as tests, so don't enable lints that would be too noisy in tests like that.
|
||||||
|
# todo = "warn"
|
||||||
|
unchecked_duration_subtraction = "warn"
|
||||||
|
used_underscore_binding = "warn"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nu-cli = { path = "./crates/nu-cli", version = "0.105.2" }
|
||||||
|
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.105.2" }
|
||||||
|
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.105.2" }
|
||||||
|
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.105.2", optional = true }
|
||||||
|
nu-cmd-extra = { path = "./crates/nu-cmd-extra", version = "0.105.2" }
|
||||||
|
nu-command = { path = "./crates/nu-command", version = "0.105.2", default-features = false, features = ["os"] }
|
||||||
|
nu-engine = { path = "./crates/nu-engine", version = "0.105.2" }
|
||||||
|
nu-explore = { path = "./crates/nu-explore", version = "0.105.2" }
|
||||||
|
nu-lsp = { path = "./crates/nu-lsp/", version = "0.105.2" }
|
||||||
|
nu-parser = { path = "./crates/nu-parser", version = "0.105.2" }
|
||||||
|
nu-path = { path = "./crates/nu-path", version = "0.105.2" }
|
||||||
|
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.105.2" }
|
||||||
|
nu-protocol = { path = "./crates/nu-protocol", version = "0.105.2" }
|
||||||
|
nu-std = { path = "./crates/nu-std", version = "0.105.2" }
|
||||||
|
nu-system = { path = "./crates/nu-system", version = "0.105.2" }
|
||||||
|
nu-utils = { path = "./crates/nu-utils", version = "0.105.2" }
|
||||||
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
|
crossterm = { workspace = true }
|
||||||
|
ctrlc = { workspace = true }
|
||||||
|
dirs = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
|
||||||
|
multipart-rs = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
simplelog = "0.12"
|
||||||
|
time = "0.3"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||||
|
# Our dependencies don't use OpenSSL on Windows
|
||||||
|
openssl = { version = "0.10", features = ["vendored"], optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
winresource = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_family = "unix")'.dependencies]
|
||||||
|
nix = { workspace = true, default-features = false, features = [
|
||||||
|
"signal",
|
||||||
|
"process",
|
||||||
|
"fs",
|
||||||
|
"term",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
nu-test-support = { path = "./crates/nu-test-support", version = "0.105.2" }
|
||||||
|
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.105.2" }
|
||||||
|
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.105.2" }
|
||||||
|
assert_cmd = "2.0"
|
||||||
|
dirs = { workspace = true }
|
||||||
|
tango-bench = "0.6"
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
|
fancy-regex = { workspace = true }
|
||||||
|
rstest = { workspace = true, default-features = false }
|
||||||
|
serial_test = "3.2"
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Enable all features while still avoiding mutually exclusive features.
|
||||||
|
# Use this if `--all-features` fails.
|
||||||
|
full = ["plugin", "rustls-tls", "system-clipboard", "trash-support", "sqlite"]
|
||||||
|
|
||||||
|
plugin = [
|
||||||
|
# crates
|
||||||
|
"dep:nu-cmd-plugin",
|
||||||
|
"dep:nu-plugin-engine",
|
||||||
|
|
||||||
|
# features
|
||||||
|
"nu-cli/plugin",
|
||||||
|
"nu-cmd-lang/plugin",
|
||||||
|
"nu-command/plugin",
|
||||||
|
"nu-engine/plugin",
|
||||||
|
"nu-engine/plugin",
|
||||||
|
"nu-parser/plugin",
|
||||||
|
"nu-protocol/plugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
native-tls = ["nu-command/native-tls"]
|
||||||
|
rustls-tls = ["nu-command/rustls-tls"]
|
||||||
|
|
||||||
|
default = [
|
||||||
|
"plugin",
|
||||||
|
"trash-support",
|
||||||
|
"sqlite",
|
||||||
|
"rustls-tls"
|
||||||
|
]
|
||||||
|
stable = ["default"]
|
||||||
|
# NOTE: individual features are also passed to `nu-cmd-lang` that uses them to generate the feature matrix in the `version` command
|
||||||
|
|
||||||
|
# Enable to statically link OpenSSL (perl is required, to build OpenSSL https://docs.rs/openssl/latest/openssl/);
|
||||||
|
# otherwise the system version will be used. Not enabled by default because it takes a while to build
|
||||||
|
static-link-openssl = ["dep:openssl"]
|
||||||
|
|
||||||
|
# Optional system clipboard support in `reedline`, this behavior has problematic compatibility with some systems.
|
||||||
|
# Missing X server/ Wayland can cause issues
|
||||||
|
system-clipboard = [
|
||||||
|
"reedline/system_clipboard",
|
||||||
|
"nu-cli/system-clipboard",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Stable (Default)
|
||||||
|
trash-support = ["nu-command/trash-support"]
|
||||||
|
|
||||||
|
# SQLite commands for nushell
|
||||||
|
sqlite = ["nu-command/sqlite", "nu-std/sqlite"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s" # Optimize for size
|
||||||
|
strip = "debuginfo"
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
# build with `cargo build --profile profiling`
|
||||||
|
# to analyze performance with tooling like linux perf
|
||||||
|
[profile.profiling]
|
||||||
|
inherits = "release"
|
||||||
|
strip = false
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# build with `cargo build --profile ci`
|
||||||
|
# to analyze performance with tooling like linux perf
|
||||||
|
[profile.ci]
|
||||||
|
inherits = "dev"
|
||||||
|
strip = false
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
# Main nu binary
|
||||||
|
[[bin]]
|
||||||
|
name = "nu"
|
||||||
|
path = "src/main.rs"
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
# 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
|
||||||
|
[patch.crates-io]
|
||||||
|
# reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
|
||||||
|
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
|
||||||
|
|
||||||
|
# Run all benchmarks with `cargo bench`
|
||||||
|
# Run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
|
||||||
|
[[bench]]
|
||||||
|
name = "benchmarks"
|
||||||
|
harness = false
|
18
nushell/Cross.toml
Normal file
18
nushell/Cross.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Configuration for cross-rs: https://github.com/cross-rs/cross
|
||||||
|
# Run cross-rs like this:
|
||||||
|
# cross build --target aarch64-unknown-linux-gnu --release
|
||||||
|
# or
|
||||||
|
# cross build --target aarch64-unknown-linux-musl --release --features=static-link-openssl
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
pre-build = [
|
||||||
|
"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]
|
||||||
|
pre-build = [
|
||||||
|
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||||
|
"apt-get update && apt-get install --assume-yes clang"
|
||||||
|
]
|
21
nushell/LICENSE
Normal file
21
nushell/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 - 2025 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.
|
237
nushell/README.md
Normal file
237
nushell/README.md
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
# Nushell <!-- omit in toc -->
|
||||||
|
[](https://crates.io/crates/nu)
|
||||||
|
[](https://github.com/nushell/nushell/actions)
|
||||||
|
[](https://github.com/nushell/nushell/actions/workflows/nightly-build.yml)
|
||||||
|
[](https://discord.gg/NtAbbGn)
|
||||||
|
[](https://changelog.com/podcast/363)
|
||||||
|
[](https://github.com/nushell/nushell/graphs/commit-activity)
|
||||||
|
[](https://github.com/nushell/nushell/graphs/contributors)
|
||||||
|
|
||||||
|
A new type of shell.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Table of Contents <!-- omit in toc -->
|
||||||
|
|
||||||
|
- [Status](#status)
|
||||||
|
- [Learning About Nu](#learning-about-nu)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Philosophy](#philosophy)
|
||||||
|
- [Pipelines](#pipelines)
|
||||||
|
- [Opening files](#opening-files)
|
||||||
|
- [Plugins](#plugins)
|
||||||
|
- [Goals](#goals)
|
||||||
|
- [Officially Supported By](#officially-supported-by)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This project has reached a minimum-viable-product level of quality. Many people use it as their daily driver, but it may be unstable for some commands. Nu's design is subject to change as it matures.
|
||||||
|
|
||||||
|
## Learning About Nu
|
||||||
|
|
||||||
|
The [Nushell book](https://www.nushell.sh/book/) is the primary source of Nushell documentation. You can find [a full list of Nu commands in the book](https://www.nushell.sh/commands/), and we have many examples of using Nu in our [cookbook](https://www.nushell.sh/cookbook/).
|
||||||
|
|
||||||
|
We're also active on [Discord](https://discord.gg/NtAbbGn); come and chat with us!
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To quickly install Nu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux and macOS
|
||||||
|
brew install nushell
|
||||||
|
# Windows
|
||||||
|
winget install nushell
|
||||||
|
```
|
||||||
|
|
||||||
|
To use `Nu` in GitHub Action, check [setup-nu](https://github.com/marketplace/actions/setup-nu) for more detail.
|
||||||
|
|
||||||
|
Detailed installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). Nu is available via many package managers:
|
||||||
|
|
||||||
|
[](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/default_files)
|
||||||
|
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
|
||||||
|
|
||||||
|
Nu draws inspiration from projects like PowerShell, functional programming languages, and modern CLI tools.
|
||||||
|
Rather than thinking of files and data as raw streams of text, Nu looks at each input as something with structure.
|
||||||
|
For example, when you list the contents of a directory what you get back is a table of rows, where each row represents an item in that directory.
|
||||||
|
These values can be piped through a series of steps, in a series of commands called a 'pipeline'.
|
||||||
|
|
||||||
|
### Pipelines
|
||||||
|
|
||||||
|
In Unix, it's common to pipe between commands to split up a sophisticated command over multiple steps.
|
||||||
|
Nu takes this a step further and builds heavily on the idea of _pipelines_.
|
||||||
|
As in the Unix philosophy, Nu allows commands to output to stdout and read from stdin.
|
||||||
|
Additionally, commands can output structured data (you can think of this as a third kind of stream).
|
||||||
|
Commands that work in the pipeline fit into one of three categories:
|
||||||
|
|
||||||
|
- Commands that produce a stream (e.g., `ls`)
|
||||||
|
- Commands that filter a stream (e.g., `where type == "dir"`)
|
||||||
|
- Commands that consume the output of the pipeline (e.g., `table`)
|
||||||
|
|
||||||
|
Commands are separated by the pipe symbol (`|`) to denote a pipeline flowing left to right.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ls | where type == "dir" | table
|
||||||
|
# => ╭────┬──────────┬──────┬─────────┬───────────────╮
|
||||||
|
# => │ # │ name │ type │ size │ modified │
|
||||||
|
# => ├────┼──────────┼──────┼─────────┼───────────────┤
|
||||||
|
# => │ 0 │ .cargo │ dir │ 0 B │ 9 minutes ago │
|
||||||
|
# => │ 1 │ assets │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 2 │ crates │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||||
|
# => │ 3 │ docker │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 4 │ docs │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 5 │ images │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 6 │ pkg_mgrs │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 7 │ samples │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => │ 8 │ src │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||||
|
# => │ 9 │ target │ dir │ 0 B │ a day ago │
|
||||||
|
# => │ 10 │ tests │ dir │ 4.0 KiB │ 2 weeks ago │
|
||||||
|
# => │ 11 │ wix │ dir │ 0 B │ 2 weeks ago │
|
||||||
|
# => ╰────┴──────────┴──────┴─────────┴───────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
Because most of the time you'll want to see the output of a pipeline, `table` is assumed.
|
||||||
|
We could have also written the above:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ls | where type == "dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
Being able to use the same commands and compose them differently is an important philosophy in Nu.
|
||||||
|
For example, we could use the built-in `ps` command to get a list of the running processes, using the same `where` as above.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ps | where cpu > 0
|
||||||
|
# => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮
|
||||||
|
# => │ # │ pid │ name │ cpu │ mem │ virtual │
|
||||||
|
# => ├───┼───────┼───────────┼───────┼───────────┼───────────┤
|
||||||
|
# => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │
|
||||||
|
# => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │
|
||||||
|
# => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │
|
||||||
|
# => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opening files
|
||||||
|
|
||||||
|
Nu can load file and URL contents as raw text or structured data (if it recognizes the format).
|
||||||
|
For example, you can load a .toml file as structured data and explore it:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
open Cargo.toml
|
||||||
|
# => ╭──────────────────┬────────────────────╮
|
||||||
|
# => │ bin │ [table 1 row] │
|
||||||
|
# => │ dependencies │ {record 25 fields} │
|
||||||
|
# => │ dev-dependencies │ {record 8 fields} │
|
||||||
|
# => │ features │ {record 10 fields} │
|
||||||
|
# => │ package │ {record 13 fields} │
|
||||||
|
# => │ patch │ {record 1 field} │
|
||||||
|
# => │ profile │ {record 3 fields} │
|
||||||
|
# => │ target │ {record 3 fields} │
|
||||||
|
# => │ workspace │ {record 1 field} │
|
||||||
|
# => ╰──────────────────┴────────────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
We can pipe this into a command that gets the contents of one of the columns:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
open Cargo.toml | get package
|
||||||
|
# => ╭───────────────┬────────────────────────────────────╮
|
||||||
|
# => │ authors │ [list 1 item] │
|
||||||
|
# => │ default-run │ nu │
|
||||||
|
# => │ description │ A new type of shell │
|
||||||
|
# => │ documentation │ https://www.nushell.sh/book/ │
|
||||||
|
# => │ edition │ 2018 │
|
||||||
|
# => │ exclude │ [list 1 item] │
|
||||||
|
# => │ homepage │ https://www.nushell.sh │
|
||||||
|
# => │ license │ MIT │
|
||||||
|
# => │ metadata │ {record 1 field} │
|
||||||
|
# => │ name │ nu │
|
||||||
|
# => │ repository │ https://github.com/nushell/nushell │
|
||||||
|
# => │ rust-version │ 1.60 │
|
||||||
|
# => │ version │ 0.72.0 │
|
||||||
|
# => ╰───────────────┴────────────────────────────────────╯
|
||||||
|
```
|
||||||
|
|
||||||
|
And if needed we can drill down further:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
open Cargo.toml | get package.version
|
||||||
|
# => 0.72.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
Nu supports plugins that offer additional functionality to the shell and follow the same structured data model that built-in commands use. There are a few examples in the `crates/nu_plugins_*` directories.
|
||||||
|
|
||||||
|
Plugins are binaries that are available in your path and follow a `nu_plugin_*` naming convention.
|
||||||
|
These binaries interact with nu via a simple JSON-RPC protocol where the command identifies itself and passes along its configuration, making it available for use.
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
topics that have been presented.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
- Nu ensures compatibility with existing platform-specific executables.
|
||||||
|
|
||||||
|
- Nu's workflow and tools should have the usability expected of modern software in 2022 (and beyond).
|
||||||
|
|
||||||
|
- Nu views data as either structured or unstructured. It is a structured shell like PowerShell.
|
||||||
|
|
||||||
|
- Finally, Nu views data functionally. Rather than using mutation, pipelines act as a means to load, change, and save data without mutable state.
|
||||||
|
|
||||||
|
## Officially Supported By
|
||||||
|
|
||||||
|
Please submit an issue or PR to be added to this list.
|
||||||
|
|
||||||
|
- [zoxide](https://github.com/ajeetdsouza/zoxide)
|
||||||
|
- [starship](https://github.com/starship/starship)
|
||||||
|
- [oh-my-posh](https://ohmyposh.dev)
|
||||||
|
- [Couchbase Shell](https://couchbase.sh)
|
||||||
|
- [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)
|
||||||
|
- [x-cmd](https://x-cmd.com/mod/nu)
|
||||||
|
- [vfox](https://github.com/version-fox/vfox)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [Contributing](CONTRIBUTING.md) for details. Thanks to all the people who already contributed!
|
||||||
|
|
||||||
|
<a href="https://github.com/nushell/nushell/graphs/contributors">
|
||||||
|
<img src="https://contributors-img.web.app/image?repo=nushell/nushell&max=750&columns=20" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The project is made available under the MIT license. See the `LICENSE` file for more information.
|
29
nushell/SECURITY.md
Normal file
29
nushell/SECURITY.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
As a shell and programming language Nushell provides you with great powers and the potential to do dangerous things to your computer and data. Whenever there is a risk that a malicious actor can abuse a bug or a violation of documented behavior/assumptions in Nushell to harm you this is a *security* risk.
|
||||||
|
We want to fix those issues without exposing our users to unnecessary risk. Thus we want to explain our security policy.
|
||||||
|
Additional issues may be part of *safety* where the behavior of Nushell as designed and implemented can cause unintended harm or a bug causes damage without the involvement of a third party.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
As Nushell is still under very active pre-stable development, the only version the core team prioritizes for security and safety fixes is the [most recent version as published on GitHub](https://github.com/nushell/nushell/releases/latest).
|
||||||
|
Only if you provide a strong reasoning and the necessary resources, will we consider blessing a backported fix with an official patch release for a previous version.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you suspect that a bug or behavior of Nushell can affect security or may be potentially exploitable, please report the issue to us in private.
|
||||||
|
Either reach out to the core team on [our Discord server](https://discord.gg/NtAbbGn) to arrange a private channel or use the [GitHub vulnerability reporting form](https://github.com/nushell/nushell/security/advisories/new).
|
||||||
|
Please try to answer the following questions:
|
||||||
|
- How can we reach you for further questions?
|
||||||
|
- What is the bug? Which system of Nushell may be affected?
|
||||||
|
- Do you have proof-of-concept for a potential exploit or have you observed an exploit in the wild?
|
||||||
|
- What is your assessment of the severity based on what could be impacted should the bug be exploited?
|
||||||
|
- Are additional people aware of the issue or deserve credit for identifying the issue?
|
||||||
|
|
||||||
|
We will try to get back to you within a week with:
|
||||||
|
- acknowledging the receipt of the report
|
||||||
|
- an initial plan of how we want to address this including the primary points of contact for further communication
|
||||||
|
- our preliminary assessment of how severe we judge the issue
|
||||||
|
- a proposal for how we can coordinate responsible disclosure (e.g. how we ship the bugfix, if we need to coordinate with distribution maintainers, when you can release a blog post if you want to etc.)
|
||||||
|
|
||||||
|
For purely *safety* related issues where the impact is severe by direct user action instead of malicious input or third parties, feel free to open a regular issue. If we deem that there may be an additional *security* risk on a *safety* issue we may continue discussions in a restricted forum.
|
BIN
nushell/assets/icons/nushell-original.png
Normal file
BIN
nushell/assets/icons/nushell-original.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
nushell/assets/nu_logo.ico
Normal file
BIN
nushell/assets/nu_logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
nushell/assets/nushell-autocomplete6.gif
Normal file
BIN
nushell/assets/nushell-autocomplete6.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
7
nushell/benches/README.md
Normal file
7
nushell/benches/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Divan benchmarks
|
||||||
|
|
||||||
|
These are benchmarks using [Divan](https://github.com/nvzqz/divan), a microbenchmarking tool for Rust.
|
||||||
|
|
||||||
|
Run all benchmarks with `cargo bench`
|
||||||
|
|
||||||
|
Or run individual benchmarks like `cargo bench -- <regex>` e.g. `cargo bench -- parse`
|
541
nushell/benches/benchmarks.rs
Normal file
541
nushell/benches/benchmarks.rs
Normal file
@ -0,0 +1,541 @@
|
|||||||
|
use nu_cli::{eval_source, evaluate_commands};
|
||||||
|
use nu_plugin_core::{Encoder, EncodingType};
|
||||||
|
use nu_plugin_protocol::{PluginCallResponse, PluginOutput};
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, Signals, Span, Spanned, Value,
|
||||||
|
engine::{EngineState, Stack},
|
||||||
|
};
|
||||||
|
use nu_std::load_standard_library;
|
||||||
|
use nu_utils::{get_default_config, get_default_env};
|
||||||
|
use std::{
|
||||||
|
fmt::Write,
|
||||||
|
hint::black_box,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Arc, atomic::AtomicBool},
|
||||||
|
};
|
||||||
|
use tango_bench::{IntoBenchmarks, benchmark_fn, tango_benchmarks, tango_main};
|
||||||
|
|
||||||
|
fn load_bench_commands() -> EngineState {
|
||||||
|
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_engine() -> EngineState {
|
||||||
|
let mut engine_state = load_bench_commands();
|
||||||
|
let cwd = std::env::current_dir()
|
||||||
|
.unwrap()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// parsing config.nu breaks without PWD set, so set a valid path
|
||||||
|
engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data()));
|
||||||
|
|
||||||
|
engine_state.generate_nu_constant();
|
||||||
|
|
||||||
|
engine_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) {
|
||||||
|
let mut engine = setup_engine();
|
||||||
|
let commands = Spanned {
|
||||||
|
span: Span::unknown(),
|
||||||
|
item: command.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
|
evaluate_commands(
|
||||||
|
&commands,
|
||||||
|
&mut engine,
|
||||||
|
&mut stack,
|
||||||
|
PipelineData::empty(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(stack, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a new table data with `row_cnt` rows, `col_cnt` columns.
|
||||||
|
fn encoding_test_data(row_cnt: usize, col_cnt: usize) -> Value {
|
||||||
|
let record = Value::test_record(
|
||||||
|
(0..col_cnt)
|
||||||
|
.map(|x| (format!("col_{x}"), Value::test_int(x as i64)))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Value::list(vec![record; row_cnt], Span::test_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_command(
|
||||||
|
name: impl Into<String>,
|
||||||
|
command: impl Into<String> + Clone,
|
||||||
|
stack: Stack,
|
||||||
|
engine: EngineState,
|
||||||
|
) -> impl IntoBenchmarks {
|
||||||
|
let commands = Spanned {
|
||||||
|
span: Span::unknown(),
|
||||||
|
item: command.into(),
|
||||||
|
};
|
||||||
|
[benchmark_fn(name, move |b| {
|
||||||
|
let commands = commands.clone();
|
||||||
|
let stack = stack.clone();
|
||||||
|
let engine = engine.clone();
|
||||||
|
b.iter(move || {
|
||||||
|
let mut stack = stack.clone();
|
||||||
|
let mut engine = engine.clone();
|
||||||
|
#[allow(clippy::unit_arg)]
|
||||||
|
black_box(
|
||||||
|
evaluate_commands(
|
||||||
|
&commands,
|
||||||
|
&mut engine,
|
||||||
|
&mut stack,
|
||||||
|
PipelineData::empty(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.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: usize) -> String {
|
||||||
|
let mut s = String::from("let record = { ");
|
||||||
|
for i in 0..n {
|
||||||
|
write!(s, "col_{i}: {i}, ").unwrap();
|
||||||
|
}
|
||||||
|
s.push('}');
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_nested_record_string(depth: usize) -> String {
|
||||||
|
let mut s = String::from("let record = {");
|
||||||
|
for _ in 0..depth {
|
||||||
|
s.push_str("col: {");
|
||||||
|
}
|
||||||
|
s.push_str("col_final: 0");
|
||||||
|
for _ in 0..depth {
|
||||||
|
s.push('}');
|
||||||
|
}
|
||||||
|
s.push('}');
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_example_table_nrows(n: usize) -> String {
|
||||||
|
let mut s = String::from("let table = [[foo bar baz]; ");
|
||||||
|
for i in 0..n {
|
||||||
|
s.push_str(&format!("[0, 1, {i}]"));
|
||||||
|
if i < n - 1 {
|
||||||
|
s.push_str(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push(']');
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_record_create(n: usize) -> impl IntoBenchmarks {
|
||||||
|
bench_command(
|
||||||
|
format!("record_create_{n}"),
|
||||||
|
create_flat_record_string(n),
|
||||||
|
Stack::new(),
|
||||||
|
setup_engine(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_record_flat_access(n: usize) -> 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: usize) -> 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);
|
||||||
|
bench_command(
|
||||||
|
format!("record_nested_access_{n}"),
|
||||||
|
format!("$record{} | ignore", nested_access),
|
||||||
|
stack,
|
||||||
|
engine,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_record_insert(n: usize, m: usize) -> impl IntoBenchmarks {
|
||||||
|
let setup_command = create_flat_record_string(n);
|
||||||
|
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
|
||||||
|
let mut insert = String::from("$record");
|
||||||
|
for i in n..(n + m) {
|
||||||
|
write!(insert, " | insert col_{i} {i}").unwrap();
|
||||||
|
}
|
||||||
|
insert.push_str(" | ignore");
|
||||||
|
bench_command(format!("record_insert_{n}_{m}"), insert, stack, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_table_create(n: usize) -> impl IntoBenchmarks {
|
||||||
|
bench_command(
|
||||||
|
format!("table_create_{n}"),
|
||||||
|
create_example_table_nrows(n),
|
||||||
|
Stack::new(),
|
||||||
|
setup_engine(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_table_get(n: usize) -> 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: usize) -> 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_table_insert_row(n: usize, m: usize) -> impl IntoBenchmarks {
|
||||||
|
let setup_command = create_example_table_nrows(n);
|
||||||
|
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
|
||||||
|
let mut insert = String::from("$table");
|
||||||
|
for i in n..(n + m) {
|
||||||
|
write!(insert, " | insert {i} {{ foo: 0, bar: 1, baz: {i} }}").unwrap();
|
||||||
|
}
|
||||||
|
insert.push_str(" | ignore");
|
||||||
|
bench_command(format!("table_insert_row_{n}_{m}"), insert, stack, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_table_insert_col(n: usize, m: usize) -> impl IntoBenchmarks {
|
||||||
|
let setup_command = create_example_table_nrows(n);
|
||||||
|
let (stack, engine) = setup_stack_and_engine_from_command(&setup_command);
|
||||||
|
let mut insert = String::from("$table");
|
||||||
|
for i in 0..m {
|
||||||
|
write!(insert, " | insert col_{i} {i}").unwrap();
|
||||||
|
}
|
||||||
|
insert.push_str(" | ignore");
|
||||||
|
bench_command(format!("table_insert_col_{n}_{m}"), insert, stack, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_eval_interleave(n: usize) -> 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_interrupt(n: usize) -> impl IntoBenchmarks {
|
||||||
|
let mut engine = setup_engine();
|
||||||
|
engine.set_signals(Signals::new(Arc::new(AtomicBool::new(false))));
|
||||||
|
let stack = Stack::new();
|
||||||
|
bench_command(
|
||||||
|
format!("eval_interleave_with_interrupt_{n}"),
|
||||||
|
format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"),
|
||||||
|
stack,
|
||||||
|
engine,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_eval_for(n: usize) -> 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: usize) -> 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: usize) -> 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),
|
||||||
|
bench_record_insert(1, 1),
|
||||||
|
bench_record_insert(10, 1),
|
||||||
|
bench_record_insert(100, 1),
|
||||||
|
bench_record_insert(1000, 1),
|
||||||
|
bench_record_insert(1, 10),
|
||||||
|
bench_record_insert(10, 10),
|
||||||
|
bench_record_insert(100, 10),
|
||||||
|
bench_record_insert(1000, 10),
|
||||||
|
// 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),
|
||||||
|
bench_table_insert_row(1, 1),
|
||||||
|
bench_table_insert_row(10, 1),
|
||||||
|
bench_table_insert_row(100, 1),
|
||||||
|
bench_table_insert_row(1000, 1),
|
||||||
|
bench_table_insert_row(1, 10),
|
||||||
|
bench_table_insert_row(10, 10),
|
||||||
|
bench_table_insert_row(100, 10),
|
||||||
|
bench_table_insert_row(1000, 10),
|
||||||
|
bench_table_insert_col(1, 1),
|
||||||
|
bench_table_insert_col(10, 1),
|
||||||
|
bench_table_insert_col(100, 1),
|
||||||
|
bench_table_insert_col(1000, 1),
|
||||||
|
bench_table_insert_col(1, 10),
|
||||||
|
bench_table_insert_col(10, 10),
|
||||||
|
bench_table_insert_col(100, 10),
|
||||||
|
bench_table_insert_col(1000, 10),
|
||||||
|
// Eval
|
||||||
|
// Interleave
|
||||||
|
bench_eval_interleave(100),
|
||||||
|
bench_eval_interleave(1_000),
|
||||||
|
bench_eval_interleave(10_000),
|
||||||
|
bench_eval_interleave_with_interrupt(100),
|
||||||
|
bench_eval_interleave_with_interrupt(1_000),
|
||||||
|
bench_eval_interleave_with_interrupt(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)
|
||||||
|
);
|
||||||
|
|
||||||
|
tango_main!();
|
3
nushell/clippy/wasm/clippy.toml
Normal file
3
nushell/clippy/wasm/clippy.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[[disallowed-types]]
|
||||||
|
path = "std::time::Instant"
|
||||||
|
reason = "WASM panics if used, use `web_time::Instant` instead"
|
13
nushell/crates/README.md
Normal file
13
nushell/crates/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Nushell core libraries and plugins
|
||||||
|
|
||||||
|
These sub-crates form both the foundation for Nu and a set of plugins which extend Nu with additional functionality.
|
||||||
|
|
||||||
|
Foundational libraries are split into two kinds of crates:
|
||||||
|
|
||||||
|
* Core crates - those crates that work together to build the Nushell language engine
|
||||||
|
* Support crates - a set of crates that support the engine with additional features like JSON support, ANSI support, and more.
|
||||||
|
|
||||||
|
Plugins are likewise also split into two types:
|
||||||
|
|
||||||
|
* Core plugins - plugins that provide part of the default experience of Nu, including access to the system properties, processes, and web-connectivity features.
|
||||||
|
* Extra plugins - these plugins run a wide range of different capabilities like working with different file types, charting, viewing binary data, and more.
|
54
nushell/crates/nu-cli/Cargo.toml
Normal file
54
nushell/crates/nu-cli/Cargo.toml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["The Nushell Project Developers"]
|
||||||
|
description = "CLI-related functionality for Nushell"
|
||||||
|
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cli"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
name = "nu-cli"
|
||||||
|
version = "0.105.2"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.105.2" }
|
||||||
|
nu-command = { path = "../nu-command", version = "0.105.2" }
|
||||||
|
nu-std = { path = "../nu-std", version = "0.105.2" }
|
||||||
|
nu-test-support = { path = "../nu-test-support", version = "0.105.2" }
|
||||||
|
rstest = { workspace = true, default-features = false }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nu-cmd-base = { path = "../nu-cmd-base", version = "0.105.2" }
|
||||||
|
nu-engine = { path = "../nu-engine", version = "0.105.2", features = ["os"] }
|
||||||
|
nu-glob = { path = "../nu-glob", version = "0.105.2" }
|
||||||
|
nu-path = { path = "../nu-path", version = "0.105.2" }
|
||||||
|
nu-parser = { path = "../nu-parser", version = "0.105.2" }
|
||||||
|
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.105.2", optional = true }
|
||||||
|
nu-protocol = { path = "../nu-protocol", version = "0.105.2", features = ["os"] }
|
||||||
|
nu-utils = { path = "../nu-utils", version = "0.105.2" }
|
||||||
|
nu-color-config = { path = "../nu-color-config", version = "0.105.2" }
|
||||||
|
nu-ansi-term = { workspace = true }
|
||||||
|
reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
||||||
|
|
||||||
|
chrono = { default-features = false, features = ["std"], workspace = true }
|
||||||
|
crossterm = { workspace = true }
|
||||||
|
fancy-regex = { workspace = true }
|
||||||
|
is_executable = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
||||||
|
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||||
|
nucleo-matcher = { workspace = true }
|
||||||
|
percent-encoding = { workspace = true }
|
||||||
|
sysinfo = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
|
unicode-segmentation = { workspace = true }
|
||||||
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
|
which = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
plugin = ["nu-plugin-engine"]
|
||||||
|
system-clipboard = ["reedline/system_clipboard"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
21
nushell/crates/nu-cli/LICENSE
Normal file
21
nushell/crates/nu-cli/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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.
|
7
nushell/crates/nu-cli/README.md
Normal file
7
nushell/crates/nu-cli/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
This crate implements the core functionality of the interactive Nushell REPL and interfaces with `reedline`.
|
||||||
|
Currently implements the syntax highlighting and completions logic.
|
||||||
|
Furthermore includes a few commands that are specific to `reedline`
|
||||||
|
|
||||||
|
## Internal Nushell crate
|
||||||
|
|
||||||
|
This crate implements components of Nushell and is not designed to support plugin authors or other users directly.
|
@ -0,0 +1,35 @@
|
|||||||
|
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 description(&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())
|
||||||
|
}
|
||||||
|
}
|
66
nushell/crates/nu-cli/src/commands/commandline/edit.rs
Normal file
66
nushell/crates/nu-cli/src/commands/commandline/edit.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CommandlineEdit;
|
||||||
|
|
||||||
|
impl Command for CommandlineEdit {
|
||||||
|
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 description(&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())
|
||||||
|
}
|
||||||
|
}
|
52
nushell/crates/nu-cli/src/commands/commandline/get_cursor.rs
Normal file
52
nushell/crates/nu-cli/src/commands/commandline/get_cursor.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CommandlineGetCursor;
|
||||||
|
|
||||||
|
impl Command for CommandlineGetCursor {
|
||||||
|
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 description(&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![],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
nushell/crates/nu-cli/src/commands/commandline/mod.rs
Normal file
9
nushell/crates/nu-cli/src/commands/commandline/mod.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
mod commandline_;
|
||||||
|
mod edit;
|
||||||
|
mod get_cursor;
|
||||||
|
mod set_cursor;
|
||||||
|
|
||||||
|
pub use commandline_::Commandline;
|
||||||
|
pub use edit::CommandlineEdit;
|
||||||
|
pub use get_cursor::CommandlineGetCursor;
|
||||||
|
pub use set_cursor::CommandlineSetCursor;
|
65
nushell/crates/nu-cli/src/commands/commandline/set_cursor.rs
Normal file
65
nushell/crates/nu-cli/src/commands/commandline/set_cursor.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CommandlineSetCursor;
|
||||||
|
|
||||||
|
impl Command for CommandlineSetCursor {
|
||||||
|
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 description(&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![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
nushell/crates/nu-cli/src/commands/default_context.rs
Normal file
36
nushell/crates/nu-cli/src/commands/default_context.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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,
|
||||||
|
HistoryImport,
|
||||||
|
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
|
||||||
|
}
|
9
nushell/crates/nu-cli/src/commands/history/fields.rs
Normal file
9
nushell/crates/nu-cli/src/commands/history/fields.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Each const is named after a HistoryItem field, and the value is the field name to be displayed to
|
||||||
|
// the user (or accept during import).
|
||||||
|
pub const COMMAND_LINE: &str = "command";
|
||||||
|
pub const START_TIMESTAMP: &str = "start_timestamp";
|
||||||
|
pub const HOSTNAME: &str = "hostname";
|
||||||
|
pub const CWD: &str = "cwd";
|
||||||
|
pub const EXIT_STATUS: &str = "exit_status";
|
||||||
|
pub const DURATION: &str = "duration";
|
||||||
|
pub const SESSION_ID: &str = "session_id";
|
206
nushell/crates/nu-cli/src/commands/history/history_.rs
Normal file
206
nushell/crates/nu-cli/src/commands/history/history_.rs
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{
|
||||||
|
HistoryFileFormat,
|
||||||
|
shell_error::{self, io::IoError},
|
||||||
|
};
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery,
|
||||||
|
SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct History;
|
||||||
|
|
||||||
|
impl Command for History {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"history"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Get the command history."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("history")
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
.switch("clear", "Clears out the history entries", Some('c'))
|
||||||
|
.switch(
|
||||||
|
"long",
|
||||||
|
"Show long listing of entries for sqlite history",
|
||||||
|
Some('l'),
|
||||||
|
)
|
||||||
|
.category(Category::History)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
let Some(history) = engine_state.history_config() else {
|
||||||
|
return Ok(PipelineData::empty());
|
||||||
|
};
|
||||||
|
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history`
|
||||||
|
let Some(history_path) = history.file_path() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound { span: Some(head) });
|
||||||
|
};
|
||||||
|
|
||||||
|
if call.has_flag(engine_state, stack, "clear")? {
|
||||||
|
let _ = std::fs::remove_file(history_path);
|
||||||
|
// TODO: FIXME also clear the auxiliary files when using sqlite
|
||||||
|
return Ok(PipelineData::empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
let long = call.has_flag(engine_state, stack, "long")?;
|
||||||
|
let signals = engine_state.signals().clone();
|
||||||
|
let history_reader: Option<Box<dyn ReedlineHistory>> = match history.file_format {
|
||||||
|
HistoryFileFormat::Sqlite => {
|
||||||
|
SqliteBackedHistory::with_file(history_path.clone(), None, None)
|
||||||
|
.map(|inner| {
|
||||||
|
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
||||||
|
boxed
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
HistoryFileFormat::Plaintext => {
|
||||||
|
FileBackedHistory::with_file(history.max_size as usize, history_path.clone())
|
||||||
|
.map(|inner| {
|
||||||
|
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
|
||||||
|
boxed
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match history.file_format {
|
||||||
|
HistoryFileFormat::Plaintext => Ok(history_reader
|
||||||
|
.and_then(|h| {
|
||||||
|
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(move |entries| {
|
||||||
|
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
fields::COMMAND_LINE => Value::string(entry.command_line, head),
|
||||||
|
// TODO: This name is inconsistent with create_history_record.
|
||||||
|
"index" => Value::int(idx as i64, head),
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or(IoError::new(
|
||||||
|
shell_error::io::ErrorKind::FileNotFound,
|
||||||
|
head,
|
||||||
|
history_path,
|
||||||
|
))?
|
||||||
|
.into_pipeline_data(head, signals)),
|
||||||
|
HistoryFileFormat::Sqlite => Ok(history_reader
|
||||||
|
.and_then(|h| {
|
||||||
|
h.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.map(move |entries| {
|
||||||
|
entries.into_iter().enumerate().map(move |(idx, entry)| {
|
||||||
|
create_sqlite_history_record(idx, entry, long, head)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok_or(IoError::new(
|
||||||
|
shell_error::io::ErrorKind::FileNotFound,
|
||||||
|
head,
|
||||||
|
history_path,
|
||||||
|
))?
|
||||||
|
.into_pipeline_data(head, signals)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "history | length",
|
||||||
|
description: "Get current history length",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "history | last 5",
|
||||||
|
description: "Show last 5 commands you have ran",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "history | where command =~ cargo | get command",
|
||||||
|
description: "Search all the commands from history that contains 'cargo'",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_sqlite_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
|
||||||
|
//1. Format all the values
|
||||||
|
//2. Create a record of either short or long columns and values
|
||||||
|
|
||||||
|
let item_id_value = Value::int(
|
||||||
|
entry
|
||||||
|
.id
|
||||||
|
.and_then(|id| id.to_string().parse::<i64>().ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
let start_timestamp_value = Value::date(
|
||||||
|
entry.start_timestamp.unwrap_or_default().fixed_offset(),
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
let command_value = Value::string(entry.command_line, head);
|
||||||
|
let session_id_value = Value::int(
|
||||||
|
entry
|
||||||
|
.session_id
|
||||||
|
.and_then(|id| id.to_string().parse::<i64>().ok())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
let hostname_value = Value::string(entry.hostname.unwrap_or_default(), head);
|
||||||
|
let cwd_value = Value::string(entry.cwd.unwrap_or_default(), head);
|
||||||
|
let duration_value = Value::duration(
|
||||||
|
entry
|
||||||
|
.duration
|
||||||
|
.and_then(|d| d.as_nanos().try_into().ok())
|
||||||
|
.unwrap_or(0),
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head);
|
||||||
|
let index_value = Value::int(idx as i64, head);
|
||||||
|
if long {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"item_id" => item_id_value,
|
||||||
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
|
fields::COMMAND_LINE => command_value,
|
||||||
|
fields::SESSION_ID => session_id_value,
|
||||||
|
fields::HOSTNAME => hostname_value,
|
||||||
|
fields::CWD => cwd_value,
|
||||||
|
fields::DURATION => duration_value,
|
||||||
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
|
"idx" => index_value,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
fields::START_TIMESTAMP => start_timestamp_value,
|
||||||
|
fields::COMMAND_LINE => command_value,
|
||||||
|
fields::CWD => cwd_value,
|
||||||
|
fields::DURATION => duration_value,
|
||||||
|
fields::EXIT_STATUS => exit_status_value,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
440
nushell/crates/nu-cli/src/commands/history/history_import.rs
Normal file
440
nushell/crates/nu-cli/src/commands/history/history_import.rs
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::{
|
||||||
|
HistoryFileFormat,
|
||||||
|
shell_error::{self, io::IoError},
|
||||||
|
};
|
||||||
|
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, ReedlineError, SearchQuery, SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::fields;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HistoryImport;
|
||||||
|
|
||||||
|
impl Command for HistoryImport {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"history import"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Import command line history."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"Can import history from input, either successive command lines or more detailed records. If providing records, available fields are:
|
||||||
|
command, start_timestamp, hostname, cwd, duration, exit_status.
|
||||||
|
|
||||||
|
If no input is provided, will import all history items from existing history in the other format: if current history is stored in sqlite, it will store it in plain text and vice versa.
|
||||||
|
|
||||||
|
Note that history item IDs are ignored when importing from file."#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("history import")
|
||||||
|
.category(Category::History)
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::Nothing),
|
||||||
|
(Type::String, Type::Nothing),
|
||||||
|
(Type::List(Box::new(Type::String)), Type::Nothing),
|
||||||
|
(Type::table(), Type::Nothing),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
example: "history import",
|
||||||
|
description: "Append all items from history in the other format to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "echo foo | history import",
|
||||||
|
description: "Append `foo` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
example: "[[ command_line cwd ]; [ foo /home ]] | history import",
|
||||||
|
description: "Append `foo` ran from `/home` to the current history",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let span = call.head;
|
||||||
|
let ok = Ok(Value::nothing(call.head).into_pipeline_data());
|
||||||
|
|
||||||
|
let Some(history) = engine_state.history_config() else {
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
let Some(current_history_path) = history.file_path() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound { span: span.into() });
|
||||||
|
};
|
||||||
|
if let Some(bak_path) = backup(¤t_history_path, span)? {
|
||||||
|
println!("Backed history to {}", bak_path.display());
|
||||||
|
}
|
||||||
|
match input {
|
||||||
|
PipelineData::Empty => {
|
||||||
|
let other_format = match history.file_format {
|
||||||
|
HistoryFileFormat::Sqlite => HistoryFileFormat::Plaintext,
|
||||||
|
HistoryFileFormat::Plaintext => HistoryFileFormat::Sqlite,
|
||||||
|
};
|
||||||
|
let src = new_backend(other_format, None)?;
|
||||||
|
let mut dst = new_backend(history.file_format, Some(current_history_path))?;
|
||||||
|
let items = src
|
||||||
|
.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.map_err(error_from_reedline)?
|
||||||
|
.into_iter()
|
||||||
|
.map(Ok);
|
||||||
|
import(dst.as_mut(), items)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let input = input.into_iter().map(item_from_value);
|
||||||
|
import(
|
||||||
|
new_backend(history.file_format, Some(current_history_path))?.as_mut(),
|
||||||
|
input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_backend(
|
||||||
|
format: HistoryFileFormat,
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
let path = match path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
let Some(mut path) = nu_path::nu_config_dir() else {
|
||||||
|
return Err(ShellError::ConfigDirNotFound { span: None });
|
||||||
|
};
|
||||||
|
path.push(format.default_file_name());
|
||||||
|
path.into_std_path_buf()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn map(
|
||||||
|
result: Result<impl History + 'static, ReedlineError>,
|
||||||
|
) -> Result<Box<dyn History>, ShellError> {
|
||||||
|
result
|
||||||
|
.map(|x| Box::new(x) as Box<dyn History>)
|
||||||
|
.map_err(error_from_reedline)
|
||||||
|
}
|
||||||
|
match format {
|
||||||
|
// Use a reasonably large value for maximum capacity.
|
||||||
|
HistoryFileFormat::Plaintext => map(FileBackedHistory::with_file(0xfffffff, path)),
|
||||||
|
HistoryFileFormat::Sqlite => map(SqliteBackedHistory::with_file(path, None, None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import(
|
||||||
|
dst: &mut dyn History,
|
||||||
|
src: impl Iterator<Item = Result<HistoryItem, ShellError>>,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
for item in src {
|
||||||
|
let mut item = item?;
|
||||||
|
item.id = None;
|
||||||
|
dst.save(item).map_err(error_from_reedline)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_from_reedline(e: ReedlineError) -> ShellError {
|
||||||
|
// TODO: Should we add a new ShellError variant?
|
||||||
|
ShellError::GenericError {
|
||||||
|
error: "Reedline error".to_owned(),
|
||||||
|
msg: format!("{e}"),
|
||||||
|
span: None,
|
||||||
|
help: None,
|
||||||
|
inner: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_value(v: Value) -> Result<HistoryItem, ShellError> {
|
||||||
|
let span = v.span();
|
||||||
|
match v {
|
||||||
|
Value::Record { val, .. } => item_from_record(val.into_owned(), span),
|
||||||
|
Value::String { val, .. } => Ok(HistoryItem {
|
||||||
|
command_line: val,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
}),
|
||||||
|
_ => Err(ShellError::UnsupportedInput {
|
||||||
|
msg: "Only list and record inputs are supported".to_owned(),
|
||||||
|
input: v.get_type().to_string(),
|
||||||
|
msg_span: span,
|
||||||
|
input_span: span,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item_from_record(mut rec: Record, span: Span) -> Result<HistoryItem, ShellError> {
|
||||||
|
let cmd = match rec.remove(fields::COMMAND_LINE) {
|
||||||
|
Some(v) => v.as_str()?.to_owned(),
|
||||||
|
None => {
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("missing column: {}", fields::COMMAND_LINE),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get<T>(
|
||||||
|
rec: &mut Record,
|
||||||
|
field: &'static str,
|
||||||
|
f: impl FnOnce(Value) -> Result<T, ShellError>,
|
||||||
|
) -> Result<Option<T>, ShellError> {
|
||||||
|
rec.remove(field).map(f).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
let rec = &mut rec;
|
||||||
|
let item = HistoryItem {
|
||||||
|
command_line: cmd,
|
||||||
|
id: None,
|
||||||
|
start_timestamp: get(rec, fields::START_TIMESTAMP, |v| Ok(v.as_date()?.to_utc()))?,
|
||||||
|
hostname: get(rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
cwd: get(rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?,
|
||||||
|
exit_status: get(rec, fields::EXIT_STATUS, |v| v.as_int())?,
|
||||||
|
duration: get(rec, fields::DURATION, |v| duration_from_value(v, span))?,
|
||||||
|
more_info: None,
|
||||||
|
// TODO: Currently reedline doesn't let you create session IDs.
|
||||||
|
session_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !rec.is_empty() {
|
||||||
|
let cols = rec.columns().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||||
|
return Err(ShellError::TypeMismatch {
|
||||||
|
err_message: format!("unsupported column names: {}", cols.join(", ")),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duration_from_value(v: Value, span: Span) -> Result<std::time::Duration, ShellError> {
|
||||||
|
chrono::Duration::nanoseconds(v.as_duration()?)
|
||||||
|
.to_std()
|
||||||
|
.map_err(|_| ShellError::NeedsPositiveValue { span })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_backup_path(path: &Path, span: Span) -> Result<PathBuf, ShellError> {
|
||||||
|
let Ok(mut bak_path) = path.to_path_buf().into_os_string().into_string() else {
|
||||||
|
// This isn't fundamentally problem, but trying to work with OsString is a nightmare.
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: "History path not UTF-8".to_string(),
|
||||||
|
msg: "History path must be representable as UTF-8".to_string(),
|
||||||
|
span: Some(span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
bak_path.push_str(".bak");
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(bak_path.into());
|
||||||
|
}
|
||||||
|
let base_len = bak_path.len();
|
||||||
|
for i in 1..100 {
|
||||||
|
use std::fmt::Write;
|
||||||
|
bak_path.truncate(base_len);
|
||||||
|
write!(&mut bak_path, ".{i}").unwrap();
|
||||||
|
if !Path::new(&bak_path).exists() {
|
||||||
|
return Ok(PathBuf::from(bak_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ShellError::GenericError {
|
||||||
|
error: "Too many backup files".to_string(),
|
||||||
|
msg: "Found too many existing backup files".to_string(),
|
||||||
|
span: Some(span),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup(path: &Path, span: Span) -> Result<Option<PathBuf>, ShellError> {
|
||||||
|
match path.metadata() {
|
||||||
|
Ok(md) if md.is_file() => (),
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(IoError::new_with_additional_context(
|
||||||
|
shell_error::io::ErrorKind::NotAFile,
|
||||||
|
span,
|
||||||
|
PathBuf::from(path),
|
||||||
|
"history path exists but is not a file",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||||
|
Err(e) => {
|
||||||
|
return Err(IoError::new_internal(
|
||||||
|
e,
|
||||||
|
"Could not get metadata",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bak_path = find_backup_path(path, span)?;
|
||||||
|
std::fs::copy(path, &bak_path).map_err(|err| {
|
||||||
|
IoError::new_internal(
|
||||||
|
err.not_found_as(NotFound::File),
|
||||||
|
"Could not copy backup",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Some(bak_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::DateTime;
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_string() -> Result<(), ShellError> {
|
||||||
|
let item = item_from_value(Value::string("foo", Span::unknown()))?;
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command", Value::string("foo", span)),
|
||||||
|
(
|
||||||
|
"start_timestamp",
|
||||||
|
Value::date(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap(),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("hostname", Value::string("localhost", span)),
|
||||||
|
("cwd", Value::string("/home/test", span)),
|
||||||
|
("duration", Value::duration(100_000_000, span)),
|
||||||
|
("exit_status", Value::int(42, span)),
|
||||||
|
]);
|
||||||
|
let item = item_from_value(rec).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
item,
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: Some(
|
||||||
|
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc()
|
||||||
|
),
|
||||||
|
hostname: Some("localhost".to_string()),
|
||||||
|
cwd: Some("/home/test".to_string()),
|
||||||
|
duration: Some(std::time::Duration::from_nanos(100_000_000)),
|
||||||
|
exit_status: Some(42),
|
||||||
|
|
||||||
|
session_id: None,
|
||||||
|
more_info: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_extra_field() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id_nonexistent", Value::int(1, span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_item_from_value_record_bad_type() {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = new_record(&[
|
||||||
|
("command_line", Value::string("foo", span)),
|
||||||
|
("id", Value::string("one".to_string(), span)),
|
||||||
|
]);
|
||||||
|
assert!(item_from_value(rec).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_record(rec: &[(&'static str, Value)]) -> Value {
|
||||||
|
let span = Span::unknown();
|
||||||
|
let rec = Record::from_raw_cols_vals(
|
||||||
|
rec.iter().map(|(col, _)| col.to_string()).collect(),
|
||||||
|
rec.iter().map(|(_, val)| val.clone()).collect(),
|
||||||
|
span,
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Value::record(rec, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::no_backup(&["history.dat"], "history.dat.bak")]
|
||||||
|
#[case::backup_exists(&["history.dat", "history.dat.bak"], "history.dat.bak.1")]
|
||||||
|
#[case::multiple_backups_exists( &["history.dat", "history.dat.bak", "history.dat.bak.1"], "history.dat.bak.2")]
|
||||||
|
fn test_find_backup_path(#[case] existing: &[&str], #[case] want: &str) {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
for name in existing {
|
||||||
|
std::fs::File::create_new(dir.path().join(name)).unwrap();
|
||||||
|
}
|
||||||
|
let got = find_backup_path(&dir.path().join("history.dat"), Span::test_data()).unwrap();
|
||||||
|
assert_eq!(got, dir.path().join(want))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut history = std::fs::File::create_new(dir.path().join("history.dat")).unwrap();
|
||||||
|
use std::io::Write;
|
||||||
|
write!(&mut history, "123").unwrap();
|
||||||
|
let want_bak_path = dir.path().join("history.dat.bak");
|
||||||
|
assert_eq!(
|
||||||
|
backup(&dir.path().join("history.dat"), Span::test_data()),
|
||||||
|
Ok(Some(want_bak_path.clone()))
|
||||||
|
);
|
||||||
|
let got_data = String::from_utf8(std::fs::read(want_bak_path).unwrap()).unwrap();
|
||||||
|
assert_eq!(got_data, "123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_backup_no_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let bak_path = backup(&dir.path().join("history.dat"), Span::test_data()).unwrap();
|
||||||
|
assert!(bak_path.is_none());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HistorySession;
|
||||||
|
|
||||||
|
impl Command for HistorySession {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"history session"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Get the command history session."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("history session")
|
||||||
|
.category(Category::History)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::Int)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
example: "history session",
|
||||||
|
description: "Get current history session",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
Ok(Value::int(engine_state.history_session_id, call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
8
nushell/crates/nu-cli/src/commands/history/mod.rs
Normal file
8
nushell/crates/nu-cli/src/commands/history/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mod fields;
|
||||||
|
mod history_;
|
||||||
|
mod history_import;
|
||||||
|
mod history_session;
|
||||||
|
|
||||||
|
pub use history_::History;
|
||||||
|
pub use history_import::HistoryImport;
|
||||||
|
pub use history_session::HistorySession;
|
41
nushell/crates/nu-cli/src/commands/keybindings.rs
Normal file
41
nushell/crates/nu-cli/src/commands/keybindings.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use nu_engine::{command_prelude::*, get_full_help};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Keybindings;
|
||||||
|
|
||||||
|
impl Command for Keybindings {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"keybindings"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(self.name())
|
||||||
|
.category(Category::Platform)
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::String)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Keybindings related commands."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"You must use one of the following subcommands. Using this command as-is will only produce this help message.
|
||||||
|
|
||||||
|
For more information on input and keybindings, check:
|
||||||
|
https://www.nushell.sh/book/line_editor.html"#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["shortcut", "hotkey"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
54
nushell/crates/nu-cli/src/commands/keybindings_default.rs
Normal file
54
nushell/crates/nu-cli/src/commands/keybindings_default.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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 description(&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())
|
||||||
|
}
|
||||||
|
}
|
117
nushell/crates/nu-cli/src/commands/keybindings_list.rs
Normal file
117
nushell/crates/nu-cli/src/commands/keybindings_list.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use reedline::{
|
||||||
|
get_reedline_edit_commands, get_reedline_keybinding_modifiers, get_reedline_keycodes,
|
||||||
|
get_reedline_prompt_edit_modes, get_reedline_reedline_events,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct KeybindingsList;
|
||||||
|
|
||||||
|
impl Command for KeybindingsList {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"keybindings list"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build(self.name())
|
||||||
|
.input_output_types(vec![(Type::Nothing, Type::table())])
|
||||||
|
.switch("modifiers", "list of modifiers", Some('m'))
|
||||||
|
.switch("keycodes", "list of keycodes", Some('k'))
|
||||||
|
.switch("modes", "list of edit modes", Some('o'))
|
||||||
|
.switch("events", "list of reedline event", Some('e'))
|
||||||
|
.switch("edits", "list of edit commands", Some('d'))
|
||||||
|
.category(Category::Platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"List available options that can be used to create keybindings."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Get list of key modifiers",
|
||||||
|
example: "keybindings list --modifiers",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Get list of reedline events and edit commands",
|
||||||
|
example: "keybindings list -e -d",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Get list with all the available options",
|
||||||
|
example: "keybindings list",
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let all_options = ["modifiers", "keycodes", "edits", "modes", "events"];
|
||||||
|
|
||||||
|
let presence = all_options
|
||||||
|
.iter()
|
||||||
|
.map(|option| call.has_flag(engine_state, stack, option))
|
||||||
|
.collect::<Result<Vec<_>, ShellError>>()?;
|
||||||
|
|
||||||
|
let no_option_specified = presence.iter().all(|present| !*present);
|
||||||
|
|
||||||
|
let records = all_options
|
||||||
|
.iter()
|
||||||
|
.zip(presence)
|
||||||
|
.filter(|(_, present)| no_option_specified || *present)
|
||||||
|
.flat_map(|(option, _)| get_records(option, call.head))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Value::list(records, call.head).into_pipeline_data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_records(entry_type: &str, span: Span) -> Vec<Value> {
|
||||||
|
let values = match entry_type {
|
||||||
|
"modifiers" => get_reedline_keybinding_modifiers().sorted(),
|
||||||
|
"keycodes" => get_reedline_keycodes().sorted(),
|
||||||
|
"edits" => get_reedline_edit_commands().sorted(),
|
||||||
|
"modes" => get_reedline_prompt_edit_modes().sorted(),
|
||||||
|
"events" => get_reedline_reedline_events().sorted(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|edit| edit.split('\n'))
|
||||||
|
.flat_map(|edit| edit.map(|edit| convert_to_record(edit, entry_type, span)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_to_record(edit: &str, entry_type: &str, span: Span) -> Value {
|
||||||
|
Value::record(
|
||||||
|
record! {
|
||||||
|
"type" => Value::string(entry_type, span),
|
||||||
|
"name" => Value::string(edit, span),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to sort a vec and return a vec
|
||||||
|
trait SortedImpl {
|
||||||
|
fn sorted(self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> SortedImpl for Vec<E>
|
||||||
|
where
|
||||||
|
E: std::cmp::Ord,
|
||||||
|
{
|
||||||
|
fn sorted(mut self) -> Self {
|
||||||
|
self.sort();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
200
nushell/crates/nu-cli/src/commands/keybindings_listen.rs
Normal file
200
nushell/crates/nu-cli/src/commands/keybindings_listen.rs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
use crossterm::{
|
||||||
|
QueueableCommand, event::Event, event::KeyCode, event::KeyEvent, execute, terminal,
|
||||||
|
};
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::shell_error::io::IoError;
|
||||||
|
use std::io::{Write, stdout};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct KeybindingsListen;
|
||||||
|
|
||||||
|
impl Command for KeybindingsListen {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"keybindings listen"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Get input from the user."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&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().map_err(|err| {
|
||||||
|
IoError::new_internal(
|
||||||
|
err,
|
||||||
|
"Could not disable raw mode",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
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().map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not flush stdout", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
terminal::enable_raw_mode().map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not enable raw mode", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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().map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not read event", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
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)).map_err(|err| {
|
||||||
|
IoError::new_internal(
|
||||||
|
err,
|
||||||
|
"Could not print output record",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
stdout
|
||||||
|
.queue(crossterm::style::Print("\r\n"))
|
||||||
|
.map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not print linebreak", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
stdout.flush().map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not flush", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.use_kitty_protocol {
|
||||||
|
let _ = execute!(
|
||||||
|
std::io::stdout(),
|
||||||
|
crossterm::event::PopKeyboardEnhancementFlags
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal::disable_raw_mode().map_err(|err| {
|
||||||
|
IoError::new_internal(err, "Could not disable raw mode", nu_protocol::location!())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
16
nushell/crates/nu-cli/src/commands/mod.rs
Normal file
16
nushell/crates/nu-cli/src/commands/mod.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
mod commandline;
|
||||||
|
mod default_context;
|
||||||
|
mod history;
|
||||||
|
mod keybindings;
|
||||||
|
mod keybindings_default;
|
||||||
|
mod keybindings_list;
|
||||||
|
mod keybindings_listen;
|
||||||
|
|
||||||
|
pub use commandline::{Commandline, CommandlineEdit, CommandlineGetCursor, CommandlineSetCursor};
|
||||||
|
pub use history::{History, HistoryImport, HistorySession};
|
||||||
|
pub use keybindings::Keybindings;
|
||||||
|
pub use keybindings_default::KeybindingsDefault;
|
||||||
|
pub use keybindings_list::KeybindingsList;
|
||||||
|
pub use keybindings_listen::KeybindingsListen;
|
||||||
|
|
||||||
|
pub use default_context::add_cli_context;
|
@ -0,0 +1,85 @@
|
|||||||
|
use super::{SemanticSuggestion, completion_options::NuMatcher};
|
||||||
|
use crate::{
|
||||||
|
SuggestionKind,
|
||||||
|
completions::{Completer, CompletionOptions},
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub struct AttributeCompletion;
|
||||||
|
pub struct AttributableCompletion;
|
||||||
|
|
||||||
|
impl Completer for AttributeCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
|
||||||
|
let attr_commands =
|
||||||
|
working_set.find_commands_by_predicate(|s| s.starts_with(b"attr "), true);
|
||||||
|
|
||||||
|
for (decl_id, name, desc, ty) in attr_commands {
|
||||||
|
let name = name.strip_prefix(b"attr ").unwrap_or(&name);
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: String::from_utf8_lossy(name).into_owned(),
|
||||||
|
description: desc,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Command(ty, Some(decl_id))),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for AttributableCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
|
||||||
|
for s in ["def", "extern", "export def", "export extern"] {
|
||||||
|
let decl_id = working_set
|
||||||
|
.find_decl(s.as_bytes())
|
||||||
|
.expect("internal error, builtin declaration not found");
|
||||||
|
let cmd = working_set.get_decl(decl_id);
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: cmd.name().into(),
|
||||||
|
description: Some(cmd.description().into()),
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Command(cmd.command_type(), None)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
49
nushell/crates/nu-cli/src/completions/base.rs
Normal file
49
nushell/crates/nu-cli/src/completions/base.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use crate::completions::CompletionOptions;
|
||||||
|
use nu_protocol::{
|
||||||
|
DeclId, Span,
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub trait Completer {
|
||||||
|
/// Fetch, filter, and sort completions
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, Option<DeclId>),
|
||||||
|
Value(nu_protocol::Type),
|
||||||
|
CellPath,
|
||||||
|
Directory,
|
||||||
|
File,
|
||||||
|
Flag,
|
||||||
|
Module,
|
||||||
|
Operator,
|
||||||
|
Variable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Suggestion> for SemanticSuggestion {
|
||||||
|
fn from(suggestion: Suggestion) -> Self {
|
||||||
|
Self {
|
||||||
|
suggestion,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
153
nushell/crates/nu-cli/src/completions/cell_path_completions.rs
Normal file
153
nushell/crates/nu-cli/src/completions/cell_path_completions.rs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||||
|
use nu_engine::{column::get_columns, eval_variable};
|
||||||
|
use nu_protocol::{
|
||||||
|
ShellError, Span, Value,
|
||||||
|
ast::{Expr, Expression, FullCellPath, PathMember},
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
eval_const::eval_constant,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
|
pub struct CellPathCompletion<'a> {
|
||||||
|
pub full_cell_path: &'a FullCellPath,
|
||||||
|
pub position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
||||||
|
let (prefix_str, start) = match member {
|
||||||
|
PathMember::String { val, span, .. } => (val, span.start),
|
||||||
|
PathMember::Int { val, span, .. } => (&val.to_string(), span.start),
|
||||||
|
};
|
||||||
|
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str);
|
||||||
|
// strip wrapping quotes
|
||||||
|
let quotations = ['"', '\'', '`'];
|
||||||
|
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str);
|
||||||
|
(prefix_str.to_string(), Span::new(start, pos + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for CellPathCompletion<'_> {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
_prefix: impl AsRef<str>,
|
||||||
|
_span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut prefix_str = String::new();
|
||||||
|
// position at dots, e.g. `$env.config.<TAB>`
|
||||||
|
let mut span = Span::new(self.position + 1, self.position + 1);
|
||||||
|
let mut path_member_num_before_pos = 0;
|
||||||
|
for member in self.full_cell_path.tail.iter() {
|
||||||
|
if member.span().end <= self.position {
|
||||||
|
path_member_num_before_pos += 1;
|
||||||
|
} else if member.span().contains(self.position) {
|
||||||
|
(prefix_str, span) = prefix_from_path_member(member, self.position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_span = reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut matcher = NuMatcher::new(prefix_str, options);
|
||||||
|
let path_members = self
|
||||||
|
.full_cell_path
|
||||||
|
.tail
|
||||||
|
.get(0..path_member_num_before_pos)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let value = eval_cell_path(
|
||||||
|
working_set,
|
||||||
|
stack,
|
||||||
|
&self.full_cell_path.head,
|
||||||
|
path_members,
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
for suggestion in get_suggestions_by_value(&value, current_span) {
|
||||||
|
matcher.add_semantic_suggestion(suggestion);
|
||||||
|
}
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Follow cell path to get the value
|
||||||
|
/// NOTE: This is a relatively lightweight implementation,
|
||||||
|
/// so it may fail to get the exact value when the expression is complicated.
|
||||||
|
/// One failing example would be `[$foo].0`
|
||||||
|
pub(crate) fn eval_cell_path(
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
head: &Expression,
|
||||||
|
path_members: &[PathMember],
|
||||||
|
span: Span,
|
||||||
|
) -> Result<Value, ShellError> {
|
||||||
|
// evaluate the head expression to get its value
|
||||||
|
let head_value = if let Expr::Var(var_id) = head.expr {
|
||||||
|
working_set
|
||||||
|
.get_variable(var_id)
|
||||||
|
.const_val
|
||||||
|
.to_owned()
|
||||||
|
.map_or_else(
|
||||||
|
|| eval_variable(working_set.permanent_state, stack, var_id, span),
|
||||||
|
Ok,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
eval_constant(working_set, head)
|
||||||
|
}?;
|
||||||
|
head_value
|
||||||
|
.follow_cell_path(path_members)
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_suggestions_by_value(
|
||||||
|
value: &Value,
|
||||||
|
current_span: reedline::Span,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let to_suggestion = |s: String, v: Option<&Value>| {
|
||||||
|
// Check if the string needs quoting
|
||||||
|
let value = if s.is_empty()
|
||||||
|
|| s.chars()
|
||||||
|
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c)))
|
||||||
|
{
|
||||||
|
format!("{:?}", s)
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span: current_span,
|
||||||
|
description: v.map(|v| v.get_type().to_string()),
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::CellPath),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match value {
|
||||||
|
Value::Record { val, .. } => val
|
||||||
|
.columns()
|
||||||
|
.map(|s| to_suggestion(s.to_string(), val.get(s)))
|
||||||
|
.collect(),
|
||||||
|
Value::List { vals, .. } => get_columns(vals.as_slice())
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
let sub_val = vals
|
||||||
|
.first()
|
||||||
|
.and_then(|v| v.as_record().ok())
|
||||||
|
.and_then(|rv| rv.get(&s));
|
||||||
|
to_suggestion(s, sub_val)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
158
nushell/crates/nu-cli/src/completions/command_completions.rs
Normal file
158
nushell/crates/nu-cli/src/completions/command_completions.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
SuggestionKind,
|
||||||
|
completions::{Completer, CompletionOptions},
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{CommandType, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
use super::{SemanticSuggestion, completion_options::NuMatcher};
|
||||||
|
|
||||||
|
pub struct CommandCompletion {
|
||||||
|
/// Whether to include internal commands
|
||||||
|
pub internals: bool,
|
||||||
|
/// Whether to include external commands
|
||||||
|
pub externals: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandCompletion {
|
||||||
|
fn external_command_completion(
|
||||||
|
&self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
sugg_span: reedline::Span,
|
||||||
|
matched_internal: impl Fn(&str) -> bool,
|
||||||
|
matcher: &mut NuMatcher<String>,
|
||||||
|
) -> HashMap<String, SemanticSuggestion> {
|
||||||
|
let mut suggs = HashMap::new();
|
||||||
|
|
||||||
|
let paths = working_set.permanent_state.get_env_var_insensitive("path");
|
||||||
|
|
||||||
|
if let Some((_, paths)) = paths {
|
||||||
|
if let Ok(paths) = paths.as_list() {
|
||||||
|
for path in paths {
|
||||||
|
let path = path.coerce_str().unwrap_or_default();
|
||||||
|
|
||||||
|
if let Ok(mut contents) = std::fs::read_dir(path.as_ref()) {
|
||||||
|
while let Some(Ok(item)) = contents.next() {
|
||||||
|
if working_set
|
||||||
|
.permanent_state
|
||||||
|
.config
|
||||||
|
.completions
|
||||||
|
.external
|
||||||
|
.max_results
|
||||||
|
<= suggs.len() as i64
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Ok(name) = item.file_name().into_string() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = if matched_internal(&name) {
|
||||||
|
format!("^{}", name)
|
||||||
|
} else {
|
||||||
|
name.clone()
|
||||||
|
};
|
||||||
|
if suggs.contains_key(&value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// TODO: check name matching before a relative heavy IO involved
|
||||||
|
// `is_executable` for performance consideration, should avoid
|
||||||
|
// duplicated `match_aux` call for matched items in the future
|
||||||
|
if matcher.matches(&name) && is_executable::is_executable(item.path()) {
|
||||||
|
// If there's an internal command with the same name, adds ^cmd to the
|
||||||
|
// matcher so that both the internal and external command are included
|
||||||
|
matcher.add(&name, value.clone());
|
||||||
|
suggs.insert(
|
||||||
|
value.clone(),
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span: sugg_span,
|
||||||
|
append_whitespace: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Command(
|
||||||
|
CommandType::External,
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suggs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for CommandCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
|
||||||
|
let sugg_span = reedline::Span::new(span.start - offset, span.end - offset);
|
||||||
|
|
||||||
|
let mut internal_suggs = HashMap::new();
|
||||||
|
if self.internals {
|
||||||
|
let filtered_commands = working_set.find_commands_by_predicate(
|
||||||
|
|name| {
|
||||||
|
let name = String::from_utf8_lossy(name);
|
||||||
|
matcher.add(&name, name.to_string())
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
for (decl_id, name, description, typ) in filtered_commands {
|
||||||
|
let name = String::from_utf8_lossy(&name);
|
||||||
|
internal_suggs.insert(
|
||||||
|
name.to_string(),
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: name.to_string(),
|
||||||
|
description,
|
||||||
|
span: sugg_span,
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Command(typ, Some(decl_id))),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut external_suggs = if self.externals {
|
||||||
|
self.external_command_completion(
|
||||||
|
working_set,
|
||||||
|
sugg_span,
|
||||||
|
|name| internal_suggs.contains_key(name),
|
||||||
|
&mut matcher,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for cmd_name in matcher.results() {
|
||||||
|
if let Some(sugg) = internal_suggs
|
||||||
|
.remove(&cmd_name)
|
||||||
|
.or_else(|| external_suggs.remove(&cmd_name))
|
||||||
|
{
|
||||||
|
res.push(sugg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
873
nushell/crates/nu-cli/src/completions/completer.rs
Normal file
873
nushell/crates/nu-cli/src/completions/completer.rs
Normal file
@ -0,0 +1,873 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
||||||
|
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion,
|
||||||
|
ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||||
|
base::{SemanticSuggestion, SuggestionKind},
|
||||||
|
};
|
||||||
|
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||||
|
use nu_engine::eval_block;
|
||||||
|
use nu_parser::{flatten_expression, parse, parse_module_file_or_dir};
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, Span, Type, Value,
|
||||||
|
ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Used as the function `f` in find_map Traverse
|
||||||
|
///
|
||||||
|
/// returns the inner-most pipeline_element of interest
|
||||||
|
/// i.e. the one that contains given position and needs completion
|
||||||
|
fn find_pipeline_element_by_position<'a>(
|
||||||
|
expr: &'a Expression,
|
||||||
|
working_set: &'a StateWorkingSet,
|
||||||
|
pos: usize,
|
||||||
|
) -> FindMapResult<&'a Expression> {
|
||||||
|
// skip the entire expression if the position is not in it
|
||||||
|
if !expr.span.contains(pos) {
|
||||||
|
return FindMapResult::Stop;
|
||||||
|
}
|
||||||
|
let closure = |expr: &'a Expression| find_pipeline_element_by_position(expr, working_set, pos);
|
||||||
|
match &expr.expr {
|
||||||
|
Expr::Call(call) => call
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.find_map(|arg| arg.expr().and_then(|e| e.find_map(working_set, &closure)))
|
||||||
|
// if no inner call/external_call found, then this is the inner-most one
|
||||||
|
.or(Some(expr))
|
||||||
|
.map(FindMapResult::Found)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Expr::ExternalCall(head, arguments) => arguments
|
||||||
|
.iter()
|
||||||
|
.find_map(|arg| arg.expr().find_map(working_set, &closure))
|
||||||
|
.or(head.as_ref().find_map(working_set, &closure))
|
||||||
|
.or(Some(expr))
|
||||||
|
.map(FindMapResult::Found)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
// complete the operator
|
||||||
|
Expr::BinaryOp(lhs, _, rhs) => lhs
|
||||||
|
.find_map(working_set, &closure)
|
||||||
|
.or(rhs.find_map(working_set, &closure))
|
||||||
|
.or(Some(expr))
|
||||||
|
.map(FindMapResult::Found)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Expr::FullCellPath(fcp) => fcp
|
||||||
|
.head
|
||||||
|
.find_map(working_set, &closure)
|
||||||
|
.map(FindMapResult::Found)
|
||||||
|
// e.g. use std/util [<tab>
|
||||||
|
.or_else(|| {
|
||||||
|
(fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_)))
|
||||||
|
.then_some(FindMapResult::Continue)
|
||||||
|
})
|
||||||
|
.or(Some(FindMapResult::Found(expr)))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Expr::Var(_) => FindMapResult::Found(expr),
|
||||||
|
Expr::AttributeBlock(ab) => ab
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|attr| &attr.expr)
|
||||||
|
.chain(Some(ab.item.as_ref()))
|
||||||
|
.find_map(|expr| expr.find_map(working_set, &closure))
|
||||||
|
.or(Some(expr))
|
||||||
|
.map(FindMapResult::Found)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
_ => FindMapResult::Continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results.
|
||||||
|
/// This function helps to strip it
|
||||||
|
fn strip_placeholder_if_any<'a>(
|
||||||
|
working_set: &'a StateWorkingSet,
|
||||||
|
span: &Span,
|
||||||
|
strip: bool,
|
||||||
|
) -> (Span, &'a [u8]) {
|
||||||
|
let new_span = if strip {
|
||||||
|
let new_end = std::cmp::max(span.end - 1, span.start);
|
||||||
|
Span::new(span.start, new_end)
|
||||||
|
} else {
|
||||||
|
span.to_owned()
|
||||||
|
};
|
||||||
|
let prefix = working_set.get_span_contents(new_span);
|
||||||
|
(new_span, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a span with noise,
|
||||||
|
/// 1. Call `rsplit` to get the last token
|
||||||
|
/// 2. Strip the last placeholder from the token
|
||||||
|
fn strip_placeholder_with_rsplit<'a>(
|
||||||
|
working_set: &'a StateWorkingSet,
|
||||||
|
span: &Span,
|
||||||
|
predicate: impl FnMut(&u8) -> bool,
|
||||||
|
strip: bool,
|
||||||
|
) -> (Span, &'a [u8]) {
|
||||||
|
let span_content = working_set.get_span_contents(*span);
|
||||||
|
let mut prefix = span_content
|
||||||
|
.rsplit(predicate)
|
||||||
|
.next()
|
||||||
|
.unwrap_or(span_content);
|
||||||
|
let start = span.end.saturating_sub(prefix.len());
|
||||||
|
if strip && !prefix.is_empty() {
|
||||||
|
prefix = &prefix[..prefix.len() - 1];
|
||||||
|
}
|
||||||
|
let end = start + prefix.len();
|
||||||
|
(Span::new(start, end), prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NuCompleter {
|
||||||
|
engine_state: Arc<EngineState>,
|
||||||
|
stack: Stack,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common arguments required for Completer
|
||||||
|
struct Context<'a> {
|
||||||
|
working_set: &'a StateWorkingSet<'a>,
|
||||||
|
span: Span,
|
||||||
|
prefix: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For argument completion
|
||||||
|
struct PositionalArguments<'a> {
|
||||||
|
/// command name
|
||||||
|
command_head: &'a str,
|
||||||
|
/// indices of positional arguments
|
||||||
|
positional_arg_indices: Vec<usize>,
|
||||||
|
/// argument list
|
||||||
|
arguments: &'a [Argument],
|
||||||
|
/// expression of current argument
|
||||||
|
expr: &'a Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context<'_> {
|
||||||
|
fn new<'a>(
|
||||||
|
working_set: &'a StateWorkingSet,
|
||||||
|
span: Span,
|
||||||
|
prefix: &'a [u8],
|
||||||
|
offset: usize,
|
||||||
|
) -> Context<'a> {
|
||||||
|
Context {
|
||||||
|
working_set,
|
||||||
|
span,
|
||||||
|
prefix,
|
||||||
|
offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuCompleter {
|
||||||
|
pub fn new(engine_state: Arc<EngineState>, stack: Arc<Stack>) -> Self {
|
||||||
|
Self {
|
||||||
|
engine_state,
|
||||||
|
stack: Stack::with_parent(stack).reset_out_dest().collect_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
|
let offset = working_set.next_span_start();
|
||||||
|
// TODO: Callers should be trimming the line themselves
|
||||||
|
let line = if line.len() > pos { &line[..pos] } else { line };
|
||||||
|
let block = parse(
|
||||||
|
&mut working_set,
|
||||||
|
Some("completer"),
|
||||||
|
// Add a placeholder `a` to the end
|
||||||
|
format!("{}a", line).as_bytes(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For completion in LSP server.
|
||||||
|
/// We don't truncate the contents in order
|
||||||
|
/// to complete the definitions after the cursor.
|
||||||
|
///
|
||||||
|
/// And we avoid the placeholder to reuse the parsed blocks
|
||||||
|
/// cached while handling other LSP requests, e.g. diagnostics
|
||||||
|
pub fn fetch_completions_within_file(
|
||||||
|
&self,
|
||||||
|
filename: &str,
|
||||||
|
pos: usize,
|
||||||
|
contents: &str,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
|
let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
|
||||||
|
let Some(file_span) = working_set.get_span_for_filename(filename) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let offset = file_span.start;
|
||||||
|
self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_completions_by_block(
|
||||||
|
&self,
|
||||||
|
block: Arc<Block>,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
pos: usize,
|
||||||
|
offset: usize,
|
||||||
|
contents: &str,
|
||||||
|
extra_placeholder: bool,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
// Adjust offset so that the spans of the suggestions will start at the right
|
||||||
|
// place even with `only_buffer_difference: true`
|
||||||
|
let mut pos_to_search = pos + offset;
|
||||||
|
if !extra_placeholder {
|
||||||
|
pos_to_search = pos_to_search.saturating_sub(1);
|
||||||
|
}
|
||||||
|
let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| {
|
||||||
|
find_pipeline_element_by_position(expr, working_set, pos_to_search)
|
||||||
|
}) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
|
// text of element_expression
|
||||||
|
let start_offset = element_expression.span.start - offset;
|
||||||
|
let Some(text) = contents.get(start_offset..pos) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
self.complete_by_expression(
|
||||||
|
working_set,
|
||||||
|
element_expression,
|
||||||
|
offset,
|
||||||
|
pos_to_search,
|
||||||
|
text,
|
||||||
|
extra_placeholder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete given the expression of interest
|
||||||
|
/// Usually, the expression is get from `find_pipeline_element_by_position`
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `offset` - start offset of current working_set span
|
||||||
|
/// * `pos` - cursor position, should be > offset
|
||||||
|
/// * `prefix_str` - all the text before the cursor, within the `element_expression`
|
||||||
|
/// * `strip` - whether to strip the extra placeholder from a span
|
||||||
|
fn complete_by_expression(
|
||||||
|
&self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
element_expression: &Expression,
|
||||||
|
offset: usize,
|
||||||
|
pos: usize,
|
||||||
|
prefix_str: &str,
|
||||||
|
strip: bool,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut suggestions: Vec<SemanticSuggestion> = vec![];
|
||||||
|
|
||||||
|
match &element_expression.expr {
|
||||||
|
Expr::Var(_) => {
|
||||||
|
return self.variable_names_completion_helper(
|
||||||
|
working_set,
|
||||||
|
element_expression.span,
|
||||||
|
offset,
|
||||||
|
strip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Expr::FullCellPath(full_cell_path) => {
|
||||||
|
// e.g. `$e<tab>` parsed as FullCellPath
|
||||||
|
// but `$e.<tab>` without placeholder should be taken as cell_path
|
||||||
|
if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
|
||||||
|
return self.variable_names_completion_helper(
|
||||||
|
working_set,
|
||||||
|
element_expression.span,
|
||||||
|
offset,
|
||||||
|
strip,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let mut cell_path_completer = CellPathCompletion {
|
||||||
|
full_cell_path,
|
||||||
|
position: if strip { pos - 1 } else { pos },
|
||||||
|
};
|
||||||
|
let ctx = Context::new(working_set, Span::unknown(), &[], offset);
|
||||||
|
return self.process_completion(&mut cell_path_completer, &ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::BinaryOp(lhs, op, _) => {
|
||||||
|
if op.span.contains(pos) {
|
||||||
|
let mut operator_completions = OperatorCompletion {
|
||||||
|
left_hand_side: lhs.as_ref(),
|
||||||
|
};
|
||||||
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
let results = self.process_completion(&mut operator_completions, &ctx);
|
||||||
|
if !results.is_empty() {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::AttributeBlock(ab) => {
|
||||||
|
if let Some(span) = ab.attributes.iter().find_map(|attr| {
|
||||||
|
let span = attr.expr.span;
|
||||||
|
span.contains(pos).then_some(span)
|
||||||
|
}) {
|
||||||
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
return self.process_completion(&mut AttributeCompletion, &ctx);
|
||||||
|
};
|
||||||
|
let span = ab.item.span;
|
||||||
|
if span.contains(pos) {
|
||||||
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
return self.process_completion(&mut AttributableCompletion, &ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: user defined internal commands can have any length
|
||||||
|
// e.g. `def "foo -f --ff bar"`, complete by line text
|
||||||
|
// instead of relying on the parsing result in that case
|
||||||
|
Expr::Call(_) | Expr::ExternalCall(_, _) => {
|
||||||
|
let need_externals = !prefix_str.contains(' ');
|
||||||
|
let need_internals = !prefix_str.starts_with('^');
|
||||||
|
let mut span = element_expression.span;
|
||||||
|
if !need_internals {
|
||||||
|
span.start += 1;
|
||||||
|
};
|
||||||
|
suggestions.extend(self.command_completion_helper(
|
||||||
|
working_set,
|
||||||
|
span,
|
||||||
|
offset,
|
||||||
|
need_internals,
|
||||||
|
need_externals,
|
||||||
|
strip,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// unfinished argument completion for commands
|
||||||
|
match &element_expression.expr {
|
||||||
|
Expr::Call(call) => {
|
||||||
|
// NOTE: the argument to complete is not necessarily the last one
|
||||||
|
// for lsp completion, we don't trim the text,
|
||||||
|
// so that `def`s after pos can be completed
|
||||||
|
let mut positional_arg_indices = Vec::new();
|
||||||
|
for (arg_idx, arg) in call.arguments.iter().enumerate() {
|
||||||
|
let span = arg.span();
|
||||||
|
if span.contains(pos) {
|
||||||
|
// if customized completion specified, it has highest priority
|
||||||
|
if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
|
||||||
|
// for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
|
||||||
|
let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
|
||||||
|
strip_placeholder_with_rsplit(
|
||||||
|
working_set,
|
||||||
|
&span,
|
||||||
|
|b| *b == b'=' || *b == b' ',
|
||||||
|
strip,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
strip_placeholder_if_any(working_set, &span, strip)
|
||||||
|
};
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
|
||||||
|
let mut completer = CustomCompletion::new(
|
||||||
|
decl_id,
|
||||||
|
prefix_str.into(),
|
||||||
|
pos - offset,
|
||||||
|
FileCompletion,
|
||||||
|
);
|
||||||
|
|
||||||
|
suggestions.extend(self.process_completion(&mut completer, &ctx));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal arguments completion
|
||||||
|
let (new_span, prefix) =
|
||||||
|
strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
let flag_completion_helper = || {
|
||||||
|
let mut flag_completions = FlagCompletion {
|
||||||
|
decl_id: call.decl_id,
|
||||||
|
};
|
||||||
|
self.process_completion(&mut flag_completions, &ctx)
|
||||||
|
};
|
||||||
|
suggestions.extend(match arg {
|
||||||
|
// flags
|
||||||
|
Argument::Named(_) | Argument::Unknown(_)
|
||||||
|
if prefix.starts_with(b"-") =>
|
||||||
|
{
|
||||||
|
flag_completion_helper()
|
||||||
|
}
|
||||||
|
// only when `strip` == false
|
||||||
|
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
|
||||||
|
// complete according to expression type and command head
|
||||||
|
Argument::Positional(expr) => {
|
||||||
|
let command_head = working_set.get_decl(call.decl_id).name();
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
|
self.argument_completion_helper(
|
||||||
|
PositionalArguments {
|
||||||
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments: &call.arguments,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
&ctx,
|
||||||
|
suggestions.is_empty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
} else if !matches!(arg, Argument::Named(_)) {
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Expr::ExternalCall(head, arguments) => {
|
||||||
|
for (i, arg) in arguments.iter().enumerate() {
|
||||||
|
let span = arg.expr().span;
|
||||||
|
if span.contains(pos) {
|
||||||
|
// e.g. `sudo l<tab>`
|
||||||
|
// HACK: judge by index 0 is not accurate
|
||||||
|
if i == 0 {
|
||||||
|
let external_cmd = working_set.get_span_contents(head.span);
|
||||||
|
if external_cmd == b"sudo" || external_cmd == b"doas" {
|
||||||
|
let commands = self.command_completion_helper(
|
||||||
|
working_set,
|
||||||
|
span,
|
||||||
|
offset,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
strip,
|
||||||
|
);
|
||||||
|
// flags of sudo/doas can still be completed by external completer
|
||||||
|
if !commands.is_empty() {
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// resort to external completer set in config
|
||||||
|
let config = self.engine_state.get_config();
|
||||||
|
if let Some(closure) = config.completions.external.completer.as_ref() {
|
||||||
|
let mut text_spans: Vec<String> =
|
||||||
|
flatten_expression(working_set, element_expression)
|
||||||
|
.iter()
|
||||||
|
.map(|(span, _)| {
|
||||||
|
let bytes = working_set.get_span_contents(*span);
|
||||||
|
String::from_utf8_lossy(bytes).to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut new_span = span;
|
||||||
|
// strip the placeholder
|
||||||
|
if strip {
|
||||||
|
if let Some(last) = text_spans.last_mut() {
|
||||||
|
last.pop();
|
||||||
|
new_span = Span::new(span.start, span.end.saturating_sub(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(external_result) =
|
||||||
|
self.external_completion(closure, &text_spans, offset, new_span)
|
||||||
|
{
|
||||||
|
suggestions.extend(external_result);
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for external path arguments with spaces, please check issue #15790
|
||||||
|
if suggestions.is_empty() {
|
||||||
|
let (new_span, prefix) =
|
||||||
|
strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no suggestions yet, fallback to file completion
|
||||||
|
if suggestions.is_empty() {
|
||||||
|
let (new_span, prefix) = strip_placeholder_with_rsplit(
|
||||||
|
working_set,
|
||||||
|
&element_expression.span,
|
||||||
|
|c| *c == b' ',
|
||||||
|
strip,
|
||||||
|
);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
|
||||||
|
}
|
||||||
|
suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variable_names_completion_helper(
|
||||||
|
&self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
strip: bool,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
if !prefix.starts_with(b"$") {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
self.process_completion(&mut VariableCompletion, &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_completion_helper(
|
||||||
|
&self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
internals: bool,
|
||||||
|
externals: bool,
|
||||||
|
strip: bool,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let config = self.engine_state.get_config();
|
||||||
|
let mut command_completions = CommandCompletion {
|
||||||
|
internals,
|
||||||
|
externals: !internals || (externals && config.completions.external.enable),
|
||||||
|
};
|
||||||
|
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||||
|
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||||
|
self.process_completion(&mut command_completions, &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn argument_completion_helper(
|
||||||
|
&self,
|
||||||
|
argument_info: PositionalArguments,
|
||||||
|
pos: usize,
|
||||||
|
ctx: &Context,
|
||||||
|
need_fallback: bool,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let PositionalArguments {
|
||||||
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments,
|
||||||
|
expr,
|
||||||
|
} = argument_info;
|
||||||
|
// special commands
|
||||||
|
match command_head {
|
||||||
|
// complete module file/directory
|
||||||
|
"use" | "export use" | "overlay use" | "source-env"
|
||||||
|
if positional_arg_indices.len() == 1 =>
|
||||||
|
{
|
||||||
|
return self.process_completion(
|
||||||
|
&mut DotNuCompletion {
|
||||||
|
std_virtual_path: command_head != "source-env",
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// NOTE: if module file already specified,
|
||||||
|
// should parse it to get modules/commands/consts to complete
|
||||||
|
"use" | "export use" => {
|
||||||
|
let Some(Argument::Positional(Expression {
|
||||||
|
expr: Expr::String(module_name),
|
||||||
|
span,
|
||||||
|
..
|
||||||
|
})) = positional_arg_indices
|
||||||
|
.first()
|
||||||
|
.and_then(|i| arguments.get(*i))
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let module_name = module_name.as_bytes();
|
||||||
|
let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) {
|
||||||
|
Some(module_id) => (module_id, None),
|
||||||
|
None => {
|
||||||
|
let mut temp_working_set =
|
||||||
|
StateWorkingSet::new(ctx.working_set.permanent_state);
|
||||||
|
let Some(module_id) = parse_module_file_or_dir(
|
||||||
|
&mut temp_working_set,
|
||||||
|
module_name,
|
||||||
|
*span,
|
||||||
|
None,
|
||||||
|
) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
(module_id, Some(temp_working_set))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut exportable_completion = ExportableCompletion {
|
||||||
|
module_id,
|
||||||
|
temp_working_set,
|
||||||
|
};
|
||||||
|
let mut complete_on_list_items = |items: &[ListItem]| -> Vec<SemanticSuggestion> {
|
||||||
|
for item in items {
|
||||||
|
let span = item.expr().span;
|
||||||
|
if span.contains(pos) {
|
||||||
|
let offset = span.start.saturating_sub(ctx.span.start);
|
||||||
|
let end_offset =
|
||||||
|
ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1);
|
||||||
|
let new_ctx = Context::new(
|
||||||
|
ctx.working_set,
|
||||||
|
Span::new(span.start, ctx.span.end.min(span.end)),
|
||||||
|
ctx.prefix.get(offset..end_offset).unwrap_or_default(),
|
||||||
|
ctx.offset,
|
||||||
|
);
|
||||||
|
return self.process_completion(&mut exportable_completion, &new_ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
match &expr.expr {
|
||||||
|
Expr::String(_) => {
|
||||||
|
return self.process_completion(&mut exportable_completion, ctx);
|
||||||
|
}
|
||||||
|
Expr::FullCellPath(fcp) => match &fcp.head.expr {
|
||||||
|
Expr::List(items) => {
|
||||||
|
return complete_on_list_items(items);
|
||||||
|
}
|
||||||
|
_ => return vec![],
|
||||||
|
},
|
||||||
|
_ => return vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"which" => {
|
||||||
|
let mut completer = CommandCompletion {
|
||||||
|
internals: true,
|
||||||
|
externals: true,
|
||||||
|
};
|
||||||
|
return self.process_completion(&mut completer, ctx);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
// general positional arguments
|
||||||
|
let file_completion_helper = || self.process_completion(&mut FileCompletion, ctx);
|
||||||
|
match &expr.expr {
|
||||||
|
Expr::Directory(_, _) => self.process_completion(&mut DirectoryCompletion, ctx),
|
||||||
|
Expr::Filepath(_, _) | Expr::GlobPattern(_, _) => file_completion_helper(),
|
||||||
|
// fallback to file completion if necessary
|
||||||
|
_ if need_fallback => file_completion_helper(),
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the completion for a given completer
|
||||||
|
fn process_completion<T: Completer>(
|
||||||
|
&self,
|
||||||
|
completer: &mut T,
|
||||||
|
ctx: &Context,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let config = self.engine_state.get_config();
|
||||||
|
|
||||||
|
let options = CompletionOptions {
|
||||||
|
case_sensitive: config.completions.case_sensitive,
|
||||||
|
match_algorithm: config.completions.algorithm.into(),
|
||||||
|
sort: config.completions.sort,
|
||||||
|
};
|
||||||
|
|
||||||
|
completer.fetch(
|
||||||
|
ctx.working_set,
|
||||||
|
&self.stack,
|
||||||
|
String::from_utf8_lossy(ctx.prefix),
|
||||||
|
ctx.span,
|
||||||
|
ctx.offset,
|
||||||
|
&options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn external_completion(
|
||||||
|
&self,
|
||||||
|
closure: &Closure,
|
||||||
|
spans: &[String],
|
||||||
|
offset: usize,
|
||||||
|
span: Span,
|
||||||
|
) -> Option<Vec<SemanticSuggestion>> {
|
||||||
|
let block = self.engine_state.get_block(closure.block_id);
|
||||||
|
let mut callee_stack = self
|
||||||
|
.stack
|
||||||
|
.captures_to_stack_preserve_out_dest(closure.captures.clone());
|
||||||
|
|
||||||
|
// Line
|
||||||
|
if let Some(pos_arg) = block.signature.required_positional.first() {
|
||||||
|
if let Some(var_id) = pos_arg.var_id {
|
||||||
|
callee_stack.add_var(
|
||||||
|
var_id,
|
||||||
|
Value::list(
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.map(|it| Value::string(it, Span::unknown()))
|
||||||
|
.collect(),
|
||||||
|
Span::unknown(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = eval_block::<WithoutDebug>(
|
||||||
|
&self.engine_state,
|
||||||
|
&mut callee_stack,
|
||||||
|
block,
|
||||||
|
PipelineData::empty(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match result.and_then(|data| data.into_value(span)) {
|
||||||
|
Ok(Value::List { vals, .. }) => {
|
||||||
|
let result =
|
||||||
|
map_value_completions(vals.iter(), Span::new(span.start, span.end), offset);
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
Ok(Value::Nothing { .. }) => None,
|
||||||
|
Ok(value) => {
|
||||||
|
log::error!(
|
||||||
|
"External completer returned invalid value of type {}",
|
||||||
|
value.get_type().to_string()
|
||||||
|
);
|
||||||
|
Some(vec![])
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("failed to eval completer block: {err}");
|
||||||
|
Some(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReedlineCompleter for NuCompleter {
|
||||||
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
self.fetch_completions_at(line, pos)
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.suggestion)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_value_completions<'a>(
|
||||||
|
list: impl Iterator<Item = &'a Value>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
list.filter_map(move |x| {
|
||||||
|
// Match for string values
|
||||||
|
if let Ok(s) = x.coerce_string() {
|
||||||
|
return Some(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: s,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Value(x.get_type())),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match for record values
|
||||||
|
if let Ok(record) = x.as_record() {
|
||||||
|
let mut suggestion = Suggestion {
|
||||||
|
value: String::from(""), // Initialize with empty string
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
};
|
||||||
|
let mut value_type = Type::String;
|
||||||
|
|
||||||
|
// Iterate the cols looking for `value` and `description`
|
||||||
|
record.iter().for_each(|(key, value)| {
|
||||||
|
match key.as_str() {
|
||||||
|
"value" => {
|
||||||
|
value_type = value.get_type();
|
||||||
|
// Convert the value to string
|
||||||
|
if let Ok(val_str) = value.coerce_string() {
|
||||||
|
// Update the suggestion value
|
||||||
|
suggestion.value = val_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"description" => {
|
||||||
|
// Convert the value to string
|
||||||
|
if let Ok(desc_str) = value.coerce_string() {
|
||||||
|
// Update the suggestion value
|
||||||
|
suggestion.description = Some(desc_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"style" => {
|
||||||
|
// Convert the value to string
|
||||||
|
suggestion.style = match value {
|
||||||
|
Value::String { val, .. } => Some(lookup_ansi_color_style(val)),
|
||||||
|
Value::Record { .. } => Some(color_record_to_nustyle(value)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Some(SemanticSuggestion {
|
||||||
|
suggestion,
|
||||||
|
kind: Some(SuggestionKind::Value(value_type)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod completer_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_completion_helper() {
|
||||||
|
let mut engine_state =
|
||||||
|
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context());
|
||||||
|
|
||||||
|
// Custom additions
|
||||||
|
let delta = {
|
||||||
|
let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
|
||||||
|
working_set.render()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = engine_state.merge_delta(delta);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Error merging delta: {:?}",
|
||||||
|
result.err().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
|
||||||
|
let dataset = [
|
||||||
|
("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
|
||||||
|
("1.0 bit-sh", false, "b", vec![]),
|
||||||
|
("1 m", true, "m", vec!["mod"]),
|
||||||
|
("1.0 m", true, "m", vec!["mod"]),
|
||||||
|
("\"a\" s", true, "s", vec!["starts-with"]),
|
||||||
|
("sudo", false, "", Vec::new()),
|
||||||
|
("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
|
||||||
|
(" sudo", false, "", Vec::new()),
|
||||||
|
(" sudo le", true, "le", vec!["let", "length"]),
|
||||||
|
(
|
||||||
|
"ls | c",
|
||||||
|
true,
|
||||||
|
"c",
|
||||||
|
vec!["cd", "config", "const", "cp", "cal"],
|
||||||
|
),
|
||||||
|
("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
|
||||||
|
];
|
||||||
|
for (line, has_result, begins_with, expected_values) in dataset {
|
||||||
|
let result = completer.fetch_completions_at(line, line.len());
|
||||||
|
// Test whether the result is empty or not
|
||||||
|
assert_eq!(!result.is_empty(), has_result, "line: {}", line);
|
||||||
|
|
||||||
|
// Test whether the result begins with the expected value
|
||||||
|
result
|
||||||
|
.iter()
|
||||||
|
.for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
|
||||||
|
|
||||||
|
// Test whether the result contains all the expected values
|
||||||
|
assert_eq!(
|
||||||
|
result
|
||||||
|
.iter()
|
||||||
|
.map(|x| expected_values.contains(&x.suggestion.value.as_str()))
|
||||||
|
.filter(|x| *x)
|
||||||
|
.count(),
|
||||||
|
expected_values.len(),
|
||||||
|
"line: {}",
|
||||||
|
line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
401
nushell/crates/nu-cli/src/completions/completion_common.rs
Normal file
401
nushell/crates/nu-cli/src/completions/completion_common.rs
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
use super::{MatchAlgorithm, completion_options::NuMatcher};
|
||||||
|
use crate::completions::CompletionOptions;
|
||||||
|
use nu_ansi_term::Style;
|
||||||
|
use nu_engine::env_to_string;
|
||||||
|
use nu_path::dots::expand_ndots;
|
||||||
|
use nu_path::{expand_to_real_path, home_dir};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
|
use nu_utils::get_ls_colors;
|
||||||
|
use std::path::{Component, MAIN_SEPARATOR as SEP, Path, PathBuf, is_separator};
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct PathBuiltFromString {
|
||||||
|
cwd: PathBuf,
|
||||||
|
parts: Vec<String>,
|
||||||
|
isdir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively goes through paths that match a given `partial`.
|
||||||
|
/// built: State struct for a valid matching path built so far.
|
||||||
|
///
|
||||||
|
/// `want_directory`: Whether we want only directories as completion matches.
|
||||||
|
/// Some commands like `cd` can only be run on directories whereas others
|
||||||
|
/// like `ls` can be run on regular files as well.
|
||||||
|
///
|
||||||
|
/// `isdir`: whether the current partial path has a trailing slash.
|
||||||
|
/// Parsing a path string into a pathbuf loses that bit of information.
|
||||||
|
///
|
||||||
|
/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components
|
||||||
|
/// of the path matched a directory exactly.
|
||||||
|
fn complete_rec(
|
||||||
|
partial: &[&str],
|
||||||
|
built_paths: &[PathBuiltFromString],
|
||||||
|
options: &CompletionOptions,
|
||||||
|
want_directory: bool,
|
||||||
|
isdir: bool,
|
||||||
|
enable_exact_match: bool,
|
||||||
|
) -> Vec<PathBuiltFromString> {
|
||||||
|
let has_more = !partial.is_empty() && (partial.len() > 1 || isdir);
|
||||||
|
|
||||||
|
if let Some((&base, rest)) = partial.split_first() {
|
||||||
|
if base.chars().all(|c| c == '.') && has_more {
|
||||||
|
let built_paths: Vec<_> = built_paths
|
||||||
|
.iter()
|
||||||
|
.map(|built| {
|
||||||
|
let mut built = built.clone();
|
||||||
|
built.parts.push(base.to_string());
|
||||||
|
built.isdir = true;
|
||||||
|
built
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return complete_rec(
|
||||||
|
rest,
|
||||||
|
&built_paths,
|
||||||
|
options,
|
||||||
|
want_directory,
|
||||||
|
isdir,
|
||||||
|
enable_exact_match,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = partial.first().unwrap_or(&"");
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
|
||||||
|
let mut exact_match = None;
|
||||||
|
// Only relevant for case insensitive matching
|
||||||
|
let mut multiple_exact_matches = false;
|
||||||
|
for built in built_paths {
|
||||||
|
let mut path = built.cwd.clone();
|
||||||
|
for part in &built.parts {
|
||||||
|
path.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(result) = path.read_dir() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
// Symlinks to directories shouldn't have a trailing slash (#13275)
|
||||||
|
built.isdir = entry_isdir && !entry.path().is_symlink();
|
||||||
|
|
||||||
|
if !want_directory || entry_isdir {
|
||||||
|
if enable_exact_match && !multiple_exact_matches && has_more {
|
||||||
|
let matches = if options.case_sensitive {
|
||||||
|
entry_name.eq(prefix)
|
||||||
|
} else {
|
||||||
|
entry_name.eq_ignore_case(prefix)
|
||||||
|
};
|
||||||
|
if matches {
|
||||||
|
if exact_match.is_none() {
|
||||||
|
exact_match = Some(built.clone());
|
||||||
|
} else {
|
||||||
|
multiple_exact_matches = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher.add(entry_name, built);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show longer completions if we have a single exact match (#13204, #14794)
|
||||||
|
if !multiple_exact_matches {
|
||||||
|
if let Some(built) = exact_match {
|
||||||
|
return complete_rec(
|
||||||
|
&partial[1..],
|
||||||
|
&[built],
|
||||||
|
options,
|
||||||
|
want_directory,
|
||||||
|
isdir,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_more {
|
||||||
|
let mut completions = vec![];
|
||||||
|
for built in matcher.results() {
|
||||||
|
completions.extend(complete_rec(
|
||||||
|
&partial[1..],
|
||||||
|
&[built],
|
||||||
|
options,
|
||||||
|
want_directory,
|
||||||
|
isdir,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
completions
|
||||||
|
} else {
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum OriginalCwd {
|
||||||
|
None,
|
||||||
|
Home,
|
||||||
|
Prefix(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OriginalCwd {
|
||||||
|
fn apply(&self, mut p: PathBuiltFromString, path_separator: char) -> 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(&path_separator.to_string());
|
||||||
|
if p.isdir {
|
||||||
|
ret.push(path_separator);
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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 struct FileSuggestion {
|
||||||
|
pub span: nu_protocol::Span,
|
||||||
|
pub path: String,
|
||||||
|
pub style: Option<Style>,
|
||||||
|
pub is_dir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Parameters
|
||||||
|
/// * `cwds` - A list of directories in which to search. The only reason this isn't a single string
|
||||||
|
/// is because dotnu_completions searches in multiple directories at once
|
||||||
|
pub fn complete_item(
|
||||||
|
want_directory: bool,
|
||||||
|
span: nu_protocol::Span,
|
||||||
|
partial: &str,
|
||||||
|
cwds: &[impl AsRef<str>],
|
||||||
|
options: &CompletionOptions,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &Stack,
|
||||||
|
) -> Vec<FileSuggestion> {
|
||||||
|
let cleaned_partial = surround_remove(partial);
|
||||||
|
let isdir = cleaned_partial.ends_with(is_separator);
|
||||||
|
let expanded_partial = expand_ndots(Path::new(&cleaned_partial));
|
||||||
|
let should_collapse_dots = expanded_partial != Path::new(&cleaned_partial);
|
||||||
|
let mut partial = expanded_partial.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let path_separator = SEP;
|
||||||
|
#[cfg(windows)]
|
||||||
|
let path_separator = cleaned_partial
|
||||||
|
.chars()
|
||||||
|
.rfind(|c: &char| is_separator(*c))
|
||||||
|
.unwrap_or(SEP);
|
||||||
|
|
||||||
|
// Handle the trailing dot case
|
||||||
|
if cleaned_partial.ends_with(&format!("{path_separator}.")) {
|
||||||
|
partial.push_str(&format!("{path_separator}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cwd_pathbufs: Vec<_> = cwds
|
||||||
|
.iter()
|
||||||
|
.map(|cwd| Path::new(cwd.as_ref()).to_path_buf())
|
||||||
|
.collect();
|
||||||
|
let ls_colors = (engine_state.config.completions.use_ls_colors
|
||||||
|
&& engine_state.config.use_ansi_coloring.get(engine_state))
|
||||||
|
.then(|| {
|
||||||
|
let ls_colors_env_str = stack
|
||||||
|
.get_env_var(engine_state, "LS_COLORS")
|
||||||
|
.and_then(|v| env_to_string("LS_COLORS", v, engine_state, stack).ok());
|
||||||
|
get_ls_colors(ls_colors_env_str)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut cwds = cwd_pathbufs.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
|
||||||
|
cwds = vec![[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) => {
|
||||||
|
// This is kind of a hack. When joining an empty string with the rest,
|
||||||
|
// we add the slash automagically
|
||||||
|
cwds = vec![PathBuf::from(c.as_os_str())];
|
||||||
|
prefix_len = 1;
|
||||||
|
original_cwd = OriginalCwd::Prefix(String::new());
|
||||||
|
}
|
||||||
|
Some(Component::Normal(home)) if home.to_string_lossy() == "~" => {
|
||||||
|
cwds = home_dir()
|
||||||
|
.map(|dir| vec![dir.into()])
|
||||||
|
.unwrap_or(cwd_pathbufs);
|
||||||
|
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(),
|
||||||
|
&cwds
|
||||||
|
.into_iter()
|
||||||
|
.map(|cwd| PathBuiltFromString {
|
||||||
|
cwd,
|
||||||
|
parts: Vec::new(),
|
||||||
|
isdir: false,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
options,
|
||||||
|
want_directory,
|
||||||
|
isdir,
|
||||||
|
options.match_algorithm == MatchAlgorithm::Prefix,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut p| {
|
||||||
|
if should_collapse_dots {
|
||||||
|
p = collapse_ndots(p);
|
||||||
|
}
|
||||||
|
let is_dir = p.isdir;
|
||||||
|
let path = original_cwd.apply(p, path_separator);
|
||||||
|
let real_path = expand_to_real_path(&path);
|
||||||
|
let metadata = std::fs::symlink_metadata(&real_path).ok();
|
||||||
|
let style = ls_colors.as_ref().map(|lsc| {
|
||||||
|
lsc.style_for_path_with_metadata(&real_path, metadata.as_ref())
|
||||||
|
.map(lscolors::Style::to_nu_ansi_term_style)
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
FileSuggestion {
|
||||||
|
span,
|
||||||
|
path: escape_path(path),
|
||||||
|
style,
|
||||||
|
is_dir,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix files or folders with quotes or hashes
|
||||||
|
pub fn escape_path(path: String) -> String {
|
||||||
|
// make glob pattern have the highest priority.
|
||||||
|
if nu_glob::is_glob(path.as_str()) || path.contains('`') {
|
||||||
|
// expand home `~` for https://github.com/nushell/nushell/issues/13905
|
||||||
|
let pathbuf = nu_path::expand_tilde(path);
|
||||||
|
let path = pathbuf.to_string_lossy();
|
||||||
|
if path.contains('\'') {
|
||||||
|
// decide to use double quotes
|
||||||
|
// Path as Debug will do the escaping for `"`, `\`
|
||||||
|
format!("{:?}", path)
|
||||||
|
} else {
|
||||||
|
format!("'{path}'")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let contaminated =
|
||||||
|
path.contains(['\'', '"', ' ', '#', '(', ')', '{', '}', '[', ']', '|', ';']);
|
||||||
|
let maybe_flag = path.starts_with('-');
|
||||||
|
let maybe_variable = path.starts_with('$');
|
||||||
|
let maybe_number = path.parse::<f64>().is_ok();
|
||||||
|
if contaminated || maybe_flag || maybe_variable || maybe_number {
|
||||||
|
format!("`{path}`")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AdjustView {
|
||||||
|
pub prefix: String,
|
||||||
|
pub span: Span,
|
||||||
|
pub readjusted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust_if_intermediate(
|
||||||
|
prefix: &str,
|
||||||
|
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 = 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collapse multiple ".." components into n-dots.
|
||||||
|
///
|
||||||
|
/// It performs the reverse operation of `expand_ndots`, collapsing sequences of ".." into n-dots,
|
||||||
|
/// such as "..." and "....".
|
||||||
|
///
|
||||||
|
/// The resulting path will use platform-specific path separators, regardless of what path separators were used in the input.
|
||||||
|
fn collapse_ndots(path: PathBuiltFromString) -> PathBuiltFromString {
|
||||||
|
let mut result = PathBuiltFromString {
|
||||||
|
parts: Vec::with_capacity(path.parts.len()),
|
||||||
|
isdir: path.isdir,
|
||||||
|
cwd: path.cwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dot_count = 0;
|
||||||
|
|
||||||
|
for part in path.parts {
|
||||||
|
if part == ".." {
|
||||||
|
dot_count += 1;
|
||||||
|
} else {
|
||||||
|
if dot_count > 0 {
|
||||||
|
result.parts.push(".".repeat(dot_count + 1));
|
||||||
|
dot_count = 0;
|
||||||
|
}
|
||||||
|
result.parts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining dots
|
||||||
|
if dot_count > 0 {
|
||||||
|
result.parts.push(".".repeat(dot_count + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
349
nushell/crates/nu-cli/src/completions/completion_options.rs
Normal file
349
nushell/crates/nu-cli/src/completions/completion_options.rs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
use nu_parser::trim_quotes_str;
|
||||||
|
use nu_protocol::{CompletionAlgorithm, CompletionSort};
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
|
use nucleo_matcher::{
|
||||||
|
Config, Matcher, Utf32Str,
|
||||||
|
pattern::{Atom, AtomKind, CaseMatching, Normalization},
|
||||||
|
};
|
||||||
|
use std::{borrow::Cow, fmt::Display};
|
||||||
|
|
||||||
|
use super::SemanticSuggestion;
|
||||||
|
|
||||||
|
/// Describes how suggestions should be matched.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub enum MatchAlgorithm {
|
||||||
|
/// Only show suggestions which begin with the given input
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "git switch" is matched by "git sw"
|
||||||
|
Prefix,
|
||||||
|
|
||||||
|
/// Only show suggestions which have a substring matching with the given input
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "git checkout" is matched by "checkout"
|
||||||
|
Substring,
|
||||||
|
|
||||||
|
/// Only show suggestions which contain the input chars at any place
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "git checkout" is matched by "gco"
|
||||||
|
Fuzzy,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NuMatcher<'a, T> {
|
||||||
|
options: &'a CompletionOptions,
|
||||||
|
needle: String,
|
||||||
|
state: State<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum State<T> {
|
||||||
|
Prefix {
|
||||||
|
/// Holds (haystack, item)
|
||||||
|
items: Vec<(String, T)>,
|
||||||
|
},
|
||||||
|
Substring {
|
||||||
|
/// Holds (haystack, item)
|
||||||
|
items: Vec<(String, T)>,
|
||||||
|
},
|
||||||
|
Fuzzy {
|
||||||
|
matcher: Matcher,
|
||||||
|
atom: Atom,
|
||||||
|
/// Holds (haystack, item, score)
|
||||||
|
items: Vec<(String, T, u16)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters and sorts suggestions
|
||||||
|
impl<T> NuMatcher<'_, T> {
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `needle` - The text to search for
|
||||||
|
pub fn new(needle: impl AsRef<str>, options: &CompletionOptions) -> NuMatcher<T> {
|
||||||
|
let needle = trim_quotes_str(needle.as_ref());
|
||||||
|
match options.match_algorithm {
|
||||||
|
MatchAlgorithm::Prefix => {
|
||||||
|
let lowercase_needle = if options.case_sensitive {
|
||||||
|
needle.to_owned()
|
||||||
|
} else {
|
||||||
|
needle.to_folded_case()
|
||||||
|
};
|
||||||
|
NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: lowercase_needle,
|
||||||
|
state: State::Prefix { items: Vec::new() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MatchAlgorithm::Substring => {
|
||||||
|
let lowercase_needle = if options.case_sensitive {
|
||||||
|
needle.to_owned()
|
||||||
|
} else {
|
||||||
|
needle.to_folded_case()
|
||||||
|
};
|
||||||
|
NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: lowercase_needle,
|
||||||
|
state: State::Substring { items: Vec::new() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MatchAlgorithm::Fuzzy => {
|
||||||
|
let atom = Atom::new(
|
||||||
|
needle,
|
||||||
|
if options.case_sensitive {
|
||||||
|
CaseMatching::Respect
|
||||||
|
} else {
|
||||||
|
CaseMatching::Ignore
|
||||||
|
},
|
||||||
|
Normalization::Smart,
|
||||||
|
AtomKind::Fuzzy,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
NuMatcher {
|
||||||
|
options,
|
||||||
|
needle: needle.to_owned(),
|
||||||
|
state: State::Fuzzy {
|
||||||
|
matcher: Matcher::new(Config::DEFAULT),
|
||||||
|
atom,
|
||||||
|
items: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether or not the haystack matches the needle. If it does, `item` is added
|
||||||
|
/// to the list of matches (if given).
|
||||||
|
///
|
||||||
|
/// Helper to avoid code duplication between [NuMatcher::add] and [NuMatcher::matches].
|
||||||
|
fn matches_aux(&mut self, haystack: &str, item: Option<T>) -> bool {
|
||||||
|
let haystack = trim_quotes_str(haystack);
|
||||||
|
match &mut self.state {
|
||||||
|
State::Prefix { items } => {
|
||||||
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
|
Cow::Borrowed(haystack)
|
||||||
|
} else {
|
||||||
|
Cow::Owned(haystack.to_folded_case())
|
||||||
|
};
|
||||||
|
let matches = haystack_folded.starts_with(self.needle.as_str());
|
||||||
|
if matches {
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
State::Substring { items } => {
|
||||||
|
let haystack_folded = if self.options.case_sensitive {
|
||||||
|
Cow::Borrowed(haystack)
|
||||||
|
} else {
|
||||||
|
Cow::Owned(haystack.to_folded_case())
|
||||||
|
};
|
||||||
|
let matches = haystack_folded.contains(self.needle.as_str());
|
||||||
|
if matches {
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
}
|
||||||
|
State::Fuzzy {
|
||||||
|
matcher,
|
||||||
|
atom,
|
||||||
|
items,
|
||||||
|
} => {
|
||||||
|
let mut haystack_buf = Vec::new();
|
||||||
|
let haystack_utf32 = Utf32Str::new(trim_quotes_str(haystack), &mut haystack_buf);
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
let Some(score) = atom.indices(haystack_utf32, matcher, &mut indices) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
items.push((haystack.to_string(), item, score));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the given item if the given haystack matches the needle.
|
||||||
|
///
|
||||||
|
/// Returns whether the item was added.
|
||||||
|
pub fn add(&mut self, haystack: impl AsRef<str>, item: T) -> bool {
|
||||||
|
self.matches_aux(haystack.as_ref(), Some(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the haystack matches the needle.
|
||||||
|
pub fn matches(&mut self, haystack: &str) -> bool {
|
||||||
|
self.matches_aux(haystack, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all the items that matched (sorted)
|
||||||
|
pub fn results(self) -> Vec<T> {
|
||||||
|
match self.state {
|
||||||
|
State::Prefix { mut items, .. } | State::Substring { mut items, .. } => {
|
||||||
|
items.sort_by(|(haystack1, _), (haystack2, _)| {
|
||||||
|
let cmp_sensitive = haystack1.cmp(haystack2);
|
||||||
|
if self.options.case_sensitive {
|
||||||
|
cmp_sensitive
|
||||||
|
} else {
|
||||||
|
haystack1
|
||||||
|
.to_folded_case()
|
||||||
|
.cmp(&haystack2.to_folded_case())
|
||||||
|
.then(cmp_sensitive)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
items.into_iter().map(|(_, item)| item).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
State::Fuzzy { mut items, .. } => {
|
||||||
|
match self.options.sort {
|
||||||
|
CompletionSort::Alphabetical => {
|
||||||
|
items.sort_by(|(haystack1, _, _), (haystack2, _, _)| {
|
||||||
|
haystack1.cmp(haystack2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CompletionSort::Smart => {
|
||||||
|
items.sort_by(|(haystack1, _, score1), (haystack2, _, score2)| {
|
||||||
|
score2.cmp(score1).then(haystack1.cmp(haystack2))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, item, _)| item)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuMatcher<'_, SemanticSuggestion> {
|
||||||
|
pub fn add_semantic_suggestion(&mut self, sugg: SemanticSuggestion) -> bool {
|
||||||
|
let value = sugg.suggestion.value.to_string();
|
||||||
|
self.add(value, sugg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CompletionAlgorithm> for MatchAlgorithm {
|
||||||
|
fn from(value: CompletionAlgorithm) -> Self {
|
||||||
|
match value {
|
||||||
|
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
|
||||||
|
CompletionAlgorithm::Substring => MatchAlgorithm::Substring,
|
||||||
|
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for MatchAlgorithm {
|
||||||
|
type Error = InvalidMatchAlgorithm;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
match value.as_str() {
|
||||||
|
"prefix" => Ok(Self::Prefix),
|
||||||
|
"substring" => Ok(Self::Substring),
|
||||||
|
"fuzzy" => Ok(Self::Fuzzy),
|
||||||
|
_ => Err(InvalidMatchAlgorithm::Unknown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InvalidMatchAlgorithm {
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for InvalidMatchAlgorithm {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match *self {
|
||||||
|
InvalidMatchAlgorithm::Unknown => write!(f, "unknown match algorithm"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for InvalidMatchAlgorithm {}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompletionOptions {
|
||||||
|
pub case_sensitive: bool,
|
||||||
|
pub match_algorithm: MatchAlgorithm,
|
||||||
|
pub sort: CompletionSort,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CompletionOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
case_sensitive: true,
|
||||||
|
match_algorithm: MatchAlgorithm::Prefix,
|
||||||
|
sort: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use super::{CompletionOptions, MatchAlgorithm, NuMatcher};
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(MatchAlgorithm::Prefix, "example text", "", true)]
|
||||||
|
#[case(MatchAlgorithm::Prefix, "example text", "examp", true)]
|
||||||
|
#[case(MatchAlgorithm::Prefix, "example text", "text", false)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "", true)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "text", true)]
|
||||||
|
#[case(MatchAlgorithm::Substring, "example text", "mplxt", false)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "examp", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "ext", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mplxt", true)]
|
||||||
|
#[case(MatchAlgorithm::Fuzzy, "example text", "mpp", false)]
|
||||||
|
fn match_algorithm_simple(
|
||||||
|
#[case] match_algorithm: MatchAlgorithm,
|
||||||
|
#[case] haystack: &str,
|
||||||
|
#[case] needle: &str,
|
||||||
|
#[case] should_match: bool,
|
||||||
|
) {
|
||||||
|
let options = CompletionOptions {
|
||||||
|
match_algorithm,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut matcher = NuMatcher::new(needle, &options);
|
||||||
|
matcher.add(haystack, haystack);
|
||||||
|
if should_match {
|
||||||
|
assert_eq!(vec![haystack], matcher.results());
|
||||||
|
} else {
|
||||||
|
assert_ne!(vec![haystack], matcher.results());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_algorithm_fuzzy_sort_score() {
|
||||||
|
let options = CompletionOptions {
|
||||||
|
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut matcher = NuMatcher::new("fob", &options);
|
||||||
|
for item in ["foo/bar", "fob", "foo bar"] {
|
||||||
|
matcher.add(item, item);
|
||||||
|
}
|
||||||
|
// Sort by score, then in alphabetical order
|
||||||
|
assert_eq!(vec!["fob", "foo bar", "foo/bar"], matcher.results());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_algorithm_fuzzy_sort_strip() {
|
||||||
|
let options = CompletionOptions {
|
||||||
|
match_algorithm: MatchAlgorithm::Fuzzy,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut matcher = NuMatcher::new("'love spaces' ", &options);
|
||||||
|
for item in [
|
||||||
|
"'i love spaces'",
|
||||||
|
"'i love spaces' so much",
|
||||||
|
"'lovespaces' ",
|
||||||
|
] {
|
||||||
|
matcher.add(item, item);
|
||||||
|
}
|
||||||
|
// Make sure the spaces are respected
|
||||||
|
assert_eq!(vec!["'i love spaces' so much"], matcher.results());
|
||||||
|
}
|
||||||
|
}
|
168
nushell/crates/nu-cli/src/completions/custom_completions.rs
Normal file
168
nushell/crates/nu-cli/src/completions/custom_completions.rs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, MatchAlgorithm, SemanticSuggestion,
|
||||||
|
completer::map_value_completions,
|
||||||
|
};
|
||||||
|
use nu_engine::eval_call;
|
||||||
|
use nu_protocol::{
|
||||||
|
DeclId, PipelineData, Span, Type, Value,
|
||||||
|
ast::{Argument, Call, Expr, Expression},
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
|
pub struct CustomCompletion<T: Completer> {
|
||||||
|
decl_id: DeclId,
|
||||||
|
line: String,
|
||||||
|
line_pos: usize,
|
||||||
|
fallback: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Completer> CustomCompletion<T> {
|
||||||
|
pub fn new(decl_id: DeclId, line: String, line_pos: usize, fallback: T) -> Self {
|
||||||
|
Self {
|
||||||
|
decl_id,
|
||||||
|
line,
|
||||||
|
line_pos,
|
||||||
|
fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Completer> Completer for CustomCompletion<T> {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
orig_options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
// Call custom declaration
|
||||||
|
let mut stack_mut = stack.clone();
|
||||||
|
let mut eval = |engine_state: &EngineState| {
|
||||||
|
eval_call::<WithoutDebug>(
|
||||||
|
engine_state,
|
||||||
|
&mut stack_mut,
|
||||||
|
&Call {
|
||||||
|
decl_id: self.decl_id,
|
||||||
|
head: span,
|
||||||
|
arguments: vec![
|
||||||
|
Argument::Positional(Expression::new_unknown(
|
||||||
|
Expr::String(self.line.clone()),
|
||||||
|
Span::unknown(),
|
||||||
|
Type::String,
|
||||||
|
)),
|
||||||
|
Argument::Positional(Expression::new_unknown(
|
||||||
|
Expr::Int(self.line_pos as i64),
|
||||||
|
Span::unknown(),
|
||||||
|
Type::Int,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
parser_info: HashMap::new(),
|
||||||
|
},
|
||||||
|
PipelineData::empty(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let result = if self.decl_id.get() < working_set.permanent_state.num_decls() {
|
||||||
|
eval(working_set.permanent_state)
|
||||||
|
} else {
|
||||||
|
let mut engine_state = working_set.permanent_state.clone();
|
||||||
|
let _ = engine_state.merge_delta(working_set.delta.clone());
|
||||||
|
eval(&engine_state)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut completion_options = orig_options.clone();
|
||||||
|
let mut should_sort = true;
|
||||||
|
|
||||||
|
// Parse result
|
||||||
|
let suggestions = match result.and_then(|data| data.into_value(span)) {
|
||||||
|
Ok(value) => match &value {
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
let completions = val
|
||||||
|
.get("completions")
|
||||||
|
.and_then(|val| {
|
||||||
|
val.as_list()
|
||||||
|
.ok()
|
||||||
|
.map(|it| map_value_completions(it.iter(), span, offset))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let options = val.get("options");
|
||||||
|
|
||||||
|
if let Some(Value::Record { val: options, .. }) = &options {
|
||||||
|
if let Some(sort) = options.get("sort").and_then(|val| val.as_bool().ok()) {
|
||||||
|
should_sort = sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(case_sensitive) = options
|
||||||
|
.get("case_sensitive")
|
||||||
|
.and_then(|val| val.as_bool().ok())
|
||||||
|
{
|
||||||
|
completion_options.case_sensitive = case_sensitive;
|
||||||
|
}
|
||||||
|
let positional =
|
||||||
|
options.get("positional").and_then(|val| val.as_bool().ok());
|
||||||
|
if positional.is_some() {
|
||||||
|
log::warn!(
|
||||||
|
"Use of the positional option is deprecated. Use the substring match algorithm instead."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(algorithm) = options
|
||||||
|
.get("completion_algorithm")
|
||||||
|
.and_then(|option| option.coerce_string().ok())
|
||||||
|
.and_then(|option| option.try_into().ok())
|
||||||
|
{
|
||||||
|
completion_options.match_algorithm = algorithm;
|
||||||
|
if let Some(false) = positional {
|
||||||
|
if completion_options.match_algorithm == MatchAlgorithm::Prefix {
|
||||||
|
completion_options.match_algorithm = MatchAlgorithm::Substring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completions
|
||||||
|
}
|
||||||
|
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
|
||||||
|
Value::Nothing { .. } => {
|
||||||
|
return self.fallback.fetch(
|
||||||
|
working_set,
|
||||||
|
stack,
|
||||||
|
prefix,
|
||||||
|
span,
|
||||||
|
offset,
|
||||||
|
orig_options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!(
|
||||||
|
"Custom completer returned invalid value of type {}",
|
||||||
|
value.get_type().to_string()
|
||||||
|
);
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Error getting custom completions: {e}");
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut matcher = NuMatcher::new(prefix, &completion_options);
|
||||||
|
|
||||||
|
if should_sort {
|
||||||
|
for sugg in suggestions {
|
||||||
|
matcher.add_semantic_suggestion(sugg);
|
||||||
|
}
|
||||||
|
matcher.results()
|
||||||
|
} else {
|
||||||
|
suggestions
|
||||||
|
.into_iter()
|
||||||
|
.filter(|sugg| matcher.matches(&sugg.suggestion.value))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions,
|
||||||
|
completion_common::{AdjustView, adjust_if_intermediate, complete_item},
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::{SemanticSuggestion, SuggestionKind, completion_common::FileSuggestion};
|
||||||
|
|
||||||
|
pub struct DirectoryCompletion;
|
||||||
|
|
||||||
|
impl Completer for DirectoryCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let AdjustView { prefix, span, .. } =
|
||||||
|
adjust_if_intermediate(prefix.as_ref(), working_set, span);
|
||||||
|
|
||||||
|
// Filter only the folders
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let items: Vec<_> = directory_completion(
|
||||||
|
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.path,
|
||||||
|
style: x.style,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: x.span.start - offset,
|
||||||
|
end: x.span.end - offset,
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Directory),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Separate the results between hidden and non hidden
|
||||||
|
let mut hidden: Vec<SemanticSuggestion> = vec![];
|
||||||
|
let mut non_hidden: Vec<SemanticSuggestion> = vec![];
|
||||||
|
|
||||||
|
for item in items.into_iter() {
|
||||||
|
let item_path = Path::new(&item.suggestion.value);
|
||||||
|
|
||||||
|
if let Some(value) = item_path.file_name() {
|
||||||
|
if let Some(value) = value.to_str() {
|
||||||
|
if value.starts_with('.') {
|
||||||
|
hidden.push(item);
|
||||||
|
} else {
|
||||||
|
non_hidden.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the hidden folders to the non hidden vec to avoid creating a new vec
|
||||||
|
non_hidden.append(&mut hidden);
|
||||||
|
|
||||||
|
non_hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn directory_completion(
|
||||||
|
span: nu_protocol::Span,
|
||||||
|
partial: &str,
|
||||||
|
cwd: &str,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &Stack,
|
||||||
|
) -> Vec<FileSuggestion> {
|
||||||
|
complete_item(true, span, partial, &[cwd], options, engine_state, stack)
|
||||||
|
}
|
209
nushell/crates/nu-cli/src/completions/dotnu_completions.rs
Normal file
209
nushell/crates/nu-cli/src/completions/dotnu_completions.rs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
|
completion_common::{FileSuggestion, surround_remove},
|
||||||
|
completion_options::NuMatcher,
|
||||||
|
file_path_completion,
|
||||||
|
};
|
||||||
|
use nu_path::expand_tilde;
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{Stack, StateWorkingSet, VirtualPath},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
path::{MAIN_SEPARATOR_STR, PathBuf, is_separator},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DotNuCompletion {
|
||||||
|
/// e.g. use std/a<tab>
|
||||||
|
pub std_virtual_path: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for DotNuCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let prefix_str = prefix.as_ref();
|
||||||
|
let start_with_backquote = prefix_str.starts_with('`');
|
||||||
|
let end_with_backquote = prefix_str.ends_with('`');
|
||||||
|
let prefix_str = prefix_str.replace('`', "");
|
||||||
|
// e.g. `./`, `..\`, `/`
|
||||||
|
let not_lib_dirs = prefix_str
|
||||||
|
.chars()
|
||||||
|
.find(|c| *c != '.')
|
||||||
|
.is_some_and(is_separator);
|
||||||
|
let mut search_dirs: Vec<PathBuf> = vec![];
|
||||||
|
|
||||||
|
let (base, partial) = if let Some((parent, remain)) = prefix_str.rsplit_once(is_separator) {
|
||||||
|
// If prefix_str is only a word we want to search in the current dir.
|
||||||
|
// "/xx" should be split to "/" and "xx".
|
||||||
|
if parent.is_empty() {
|
||||||
|
(MAIN_SEPARATOR_STR, remain)
|
||||||
|
} else {
|
||||||
|
(parent, remain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(".", prefix_str.as_str())
|
||||||
|
};
|
||||||
|
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
|
||||||
|
|
||||||
|
// Fetch the lib dirs
|
||||||
|
// NOTE: 2 ways to setup `NU_LIB_DIRS`
|
||||||
|
// 1. `const NU_LIB_DIRS = [paths]`, equal to `nu -I paths`
|
||||||
|
// 2. `$env.NU_LIB_DIRS = [paths]`
|
||||||
|
let const_lib_dirs = working_set
|
||||||
|
.find_variable(b"$NU_LIB_DIRS")
|
||||||
|
.and_then(|vid| working_set.get_variable(vid).const_val.as_ref());
|
||||||
|
let env_lib_dirs = working_set.get_env_var("NU_LIB_DIRS");
|
||||||
|
let lib_dirs: HashSet<PathBuf> = [const_lib_dirs, env_lib_dirs]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|lib_dirs| {
|
||||||
|
lib_dirs
|
||||||
|
.as_list()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|it| it.iter().filter_map(|x| x.to_path().ok()))
|
||||||
|
.map(expand_tilde)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Check if the base_dir is a folder
|
||||||
|
let cwd = working_set.permanent_state.cwd(None);
|
||||||
|
if base_dir != "." {
|
||||||
|
let expanded_base_dir = expand_tilde(&base_dir);
|
||||||
|
let is_base_dir_relative = expanded_base_dir.is_relative();
|
||||||
|
// Search in base_dir as well as lib_dirs.
|
||||||
|
// After expanded, base_dir can be a relative path or absolute path.
|
||||||
|
// If relative, we join "current working dir" with it to get subdirectory and add to search_dirs.
|
||||||
|
// If absolute, we add it to search_dirs.
|
||||||
|
if let Ok(mut cwd) = cwd {
|
||||||
|
if is_base_dir_relative {
|
||||||
|
cwd.push(&base_dir);
|
||||||
|
search_dirs.push(cwd.into_std_path_buf());
|
||||||
|
} else {
|
||||||
|
search_dirs.push(expanded_base_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !not_lib_dirs {
|
||||||
|
search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
|
||||||
|
dir.push(&base_dir);
|
||||||
|
dir
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Ok(cwd) = cwd {
|
||||||
|
search_dirs.push(cwd.into_std_path_buf());
|
||||||
|
}
|
||||||
|
if !not_lib_dirs {
|
||||||
|
search_dirs.extend(lib_dirs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
|
// and transform them into suggestions
|
||||||
|
let mut completions = file_path_completion(
|
||||||
|
span,
|
||||||
|
partial,
|
||||||
|
&search_dirs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|d| d.to_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
options,
|
||||||
|
working_set.permanent_state,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.std_virtual_path {
|
||||||
|
let mut matcher = NuMatcher::new(partial, options);
|
||||||
|
let base_dir = surround_remove(&base_dir);
|
||||||
|
if base_dir == "." {
|
||||||
|
let surround_prefix = partial
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| "`'\"".contains(*c))
|
||||||
|
.collect::<String>();
|
||||||
|
for path in ["std", "std-rfc"] {
|
||||||
|
let path = format!("{}{}", surround_prefix, path);
|
||||||
|
matcher.add(
|
||||||
|
path.clone(),
|
||||||
|
FileSuggestion {
|
||||||
|
span,
|
||||||
|
path,
|
||||||
|
style: None,
|
||||||
|
is_dir: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(VirtualPath::Dir(sub_paths)) =
|
||||||
|
working_set.find_virtual_path(&base_dir)
|
||||||
|
{
|
||||||
|
for sub_vp_id in sub_paths {
|
||||||
|
let (path, sub_vp) = working_set.get_virtual_path(*sub_vp_id);
|
||||||
|
let path = path
|
||||||
|
.strip_prefix(&format!("{}/", base_dir))
|
||||||
|
.unwrap_or(path)
|
||||||
|
.to_string();
|
||||||
|
matcher.add(
|
||||||
|
path.clone(),
|
||||||
|
FileSuggestion {
|
||||||
|
path,
|
||||||
|
span,
|
||||||
|
style: None,
|
||||||
|
is_dir: matches!(sub_vp, VirtualPath::Dir(_)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completions.extend(matcher.results());
|
||||||
|
}
|
||||||
|
|
||||||
|
completions
|
||||||
|
.into_iter()
|
||||||
|
// Different base dir, so we list the .nu files or folders
|
||||||
|
.filter(|it| {
|
||||||
|
// for paths with spaces in them
|
||||||
|
let path = it.path.trim_end_matches('`');
|
||||||
|
path.ends_with(".nu") || it.is_dir
|
||||||
|
})
|
||||||
|
.map(|x| {
|
||||||
|
let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote);
|
||||||
|
// Re-calculate the span to replace
|
||||||
|
let mut span_offset = 0;
|
||||||
|
let mut value = x.path.to_string();
|
||||||
|
// Complete only the last path component
|
||||||
|
if base_dir == MAIN_SEPARATOR_STR {
|
||||||
|
span_offset = base_dir.len()
|
||||||
|
} else if base_dir != "." {
|
||||||
|
span_offset = base_dir.len() + 1
|
||||||
|
}
|
||||||
|
// Retain only one '`'
|
||||||
|
if start_with_backquote {
|
||||||
|
value = value.trim_start_matches('`').to_string();
|
||||||
|
span_offset += 1;
|
||||||
|
}
|
||||||
|
// Add the backquote back
|
||||||
|
if end_with_backquote && !value.ends_with('`') {
|
||||||
|
value.push('`');
|
||||||
|
}
|
||||||
|
let end = x.span.end - offset;
|
||||||
|
let start = std::cmp::min(end, x.span.start - offset + span_offset);
|
||||||
|
SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
style: x.style,
|
||||||
|
span: reedline::Span { start, end },
|
||||||
|
append_whitespace,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Module),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
}
|
112
nushell/crates/nu-cli/src/completions/exportable_completions.rs
Normal file
112
nushell/crates/nu-cli/src/completions/exportable_completions.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, SemanticSuggestion, SuggestionKind,
|
||||||
|
completion_common::surround_remove, completion_options::NuMatcher,
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
ModuleId, Span,
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub struct ExportableCompletion<'a> {
|
||||||
|
pub module_id: ModuleId,
|
||||||
|
pub temp_working_set: Option<StateWorkingSet<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If name contains space, wrap it in quotes
|
||||||
|
fn wrapped_name(name: String) -> String {
|
||||||
|
if !name.contains(' ') {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if name.contains('\'') {
|
||||||
|
format!("\"{}\"", name.replace('"', r#"\""#))
|
||||||
|
} else {
|
||||||
|
format!("'{name}'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for ExportableCompletion<'_> {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::<()>::new(surround_remove(prefix.as_ref()), options);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let span = reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
};
|
||||||
|
// TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix
|
||||||
|
let mut add_suggestion = |value: String,
|
||||||
|
description: Option<String>,
|
||||||
|
extra: Option<Vec<String>>,
|
||||||
|
kind: SuggestionKind| {
|
||||||
|
results.push(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
span,
|
||||||
|
description,
|
||||||
|
extra,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(kind),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let working_set = self.temp_working_set.as_ref().unwrap_or(working_set);
|
||||||
|
let module = working_set.get_module(self.module_id);
|
||||||
|
|
||||||
|
for (name, decl_id) in &module.decls {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let cmd = working_set.get_decl(*decl_id);
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
Some(cmd.description().to_string()),
|
||||||
|
None,
|
||||||
|
// `None` here avoids arguments being expanded by snippet edit style for lsp
|
||||||
|
SuggestionKind::Command(cmd.command_type(), None),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, module_id) in &module.submodules {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let comments = working_set.get_module_comments(*module_id).map(|spans| {
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.map(|sp| {
|
||||||
|
String::from_utf8_lossy(working_set.get_span_contents(*sp)).into()
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
});
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
Some("Submodule".into()),
|
||||||
|
comments,
|
||||||
|
SuggestionKind::Module,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, var_id) in &module.constants {
|
||||||
|
let name = String::from_utf8_lossy(name).to_string();
|
||||||
|
if matcher.matches(&name) {
|
||||||
|
let var = working_set.get_variable(*var_id);
|
||||||
|
add_suggestion(
|
||||||
|
wrapped_name(name),
|
||||||
|
var.const_val
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|v| v.clone().coerce_into_string().ok()),
|
||||||
|
None,
|
||||||
|
SuggestionKind::Variable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
97
nushell/crates/nu-cli/src/completions/file_completions.rs
Normal file
97
nushell/crates/nu-cli/src/completions/file_completions.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions,
|
||||||
|
completion_common::{AdjustView, adjust_if_intermediate, complete_item},
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::{SemanticSuggestion, SuggestionKind, completion_common::FileSuggestion};
|
||||||
|
|
||||||
|
pub struct FileCompletion;
|
||||||
|
|
||||||
|
impl Completer for FileCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let AdjustView {
|
||||||
|
prefix,
|
||||||
|
span,
|
||||||
|
readjusted,
|
||||||
|
} = adjust_if_intermediate(prefix.as_ref(), working_set, span);
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let items: 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.path,
|
||||||
|
style: x.style,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: x.span.start - offset,
|
||||||
|
end: x.span.end - offset,
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(if x.is_dir {
|
||||||
|
SuggestionKind::Directory
|
||||||
|
} else {
|
||||||
|
SuggestionKind::File
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort results prioritizing the non hidden folders
|
||||||
|
|
||||||
|
// Separate the results between hidden and non hidden
|
||||||
|
let mut hidden: Vec<SemanticSuggestion> = vec![];
|
||||||
|
let mut non_hidden: Vec<SemanticSuggestion> = vec![];
|
||||||
|
|
||||||
|
for item in items.into_iter() {
|
||||||
|
let item_path = Path::new(&item.suggestion.value);
|
||||||
|
|
||||||
|
if let Some(value) = item_path.file_name() {
|
||||||
|
if let Some(value) = value.to_str() {
|
||||||
|
if value.starts_with('.') {
|
||||||
|
hidden.push(item);
|
||||||
|
} else {
|
||||||
|
non_hidden.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the hidden folders to the non hidden vec to avoid creating a new vec
|
||||||
|
non_hidden.append(&mut hidden);
|
||||||
|
|
||||||
|
non_hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_path_completion(
|
||||||
|
span: nu_protocol::Span,
|
||||||
|
partial: &str,
|
||||||
|
cwds: &[impl AsRef<str>],
|
||||||
|
options: &CompletionOptions,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &Stack,
|
||||||
|
) -> Vec<FileSuggestion> {
|
||||||
|
complete_item(false, span, partial, cwds, options, engine_state, stack)
|
||||||
|
}
|
58
nushell/crates/nu-cli/src/completions/flag_completions.rs
Normal file
58
nushell/crates/nu-cli/src/completions/flag_completions.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, completion_options::NuMatcher,
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
DeclId, Span,
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FlagCompletion {
|
||||||
|
pub decl_id: DeclId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for FlagCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
let mut add_suggestion = |value: String, description: String| {
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value,
|
||||||
|
description: Some(description),
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Flag),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let decl = working_set.get_decl(self.decl_id);
|
||||||
|
let sig = decl.signature();
|
||||||
|
for named in &sig.named {
|
||||||
|
if let Some(short) = named.short {
|
||||||
|
let mut name = String::from("-");
|
||||||
|
name.push(short);
|
||||||
|
add_suggestion(name, named.desc.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if named.long.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
add_suggestion(format!("--{}", named.long), named.desc.clone());
|
||||||
|
}
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
30
nushell/crates/nu-cli/src/completions/mod.rs
Normal file
30
nushell/crates/nu-cli/src/completions/mod.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
mod attribute_completions;
|
||||||
|
mod base;
|
||||||
|
mod cell_path_completions;
|
||||||
|
mod command_completions;
|
||||||
|
mod completer;
|
||||||
|
mod completion_common;
|
||||||
|
mod completion_options;
|
||||||
|
mod custom_completions;
|
||||||
|
mod directory_completions;
|
||||||
|
mod dotnu_completions;
|
||||||
|
mod exportable_completions;
|
||||||
|
mod file_completions;
|
||||||
|
mod flag_completions;
|
||||||
|
mod operator_completions;
|
||||||
|
mod variable_completions;
|
||||||
|
|
||||||
|
pub use attribute_completions::{AttributableCompletion, AttributeCompletion};
|
||||||
|
pub use base::{Completer, SemanticSuggestion, SuggestionKind};
|
||||||
|
pub use cell_path_completions::CellPathCompletion;
|
||||||
|
pub use command_completions::CommandCompletion;
|
||||||
|
pub use completer::NuCompleter;
|
||||||
|
pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
||||||
|
pub use custom_completions::CustomCompletion;
|
||||||
|
pub use directory_completions::DirectoryCompletion;
|
||||||
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
|
pub use exportable_completions::ExportableCompletion;
|
||||||
|
pub use file_completions::{FileCompletion, file_path_completion};
|
||||||
|
pub use flag_completions::FlagCompletion;
|
||||||
|
pub use operator_completions::OperatorCompletion;
|
||||||
|
pub use variable_completions::VariableCompletion;
|
277
nushell/crates/nu-cli/src/completions/operator_completions.rs
Normal file
277
nushell/crates/nu-cli/src/completions/operator_completions.rs
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
use crate::completions::{
|
||||||
|
Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, completion_options::NuMatcher,
|
||||||
|
};
|
||||||
|
use nu_protocol::{
|
||||||
|
ENV_VARIABLE_ID, Span, Type, Value,
|
||||||
|
ast::{self, Comparison, Expr, Expression},
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use strum::{EnumMessage, IntoEnumIterator};
|
||||||
|
|
||||||
|
use super::cell_path_completions::eval_cell_path;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OperatorCompletion<'a> {
|
||||||
|
pub left_hand_side: &'a Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperatorItem {
|
||||||
|
pub symbols: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operator_to_item<T: EnumMessage + AsRef<str>>(op: T) -> OperatorItem {
|
||||||
|
OperatorItem {
|
||||||
|
symbols: op.as_ref().into(),
|
||||||
|
description: op.get_message().unwrap_or_default().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_comparison_ops() -> Vec<OperatorItem> {
|
||||||
|
vec![
|
||||||
|
operator_to_item(Comparison::In),
|
||||||
|
operator_to_item(Comparison::NotIn),
|
||||||
|
operator_to_item(Comparison::Equal),
|
||||||
|
operator_to_item(Comparison::NotEqual),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_ops_for_immutable() -> Vec<OperatorItem> {
|
||||||
|
ast::Comparison::iter()
|
||||||
|
.map(operator_to_item)
|
||||||
|
.chain(ast::Math::iter().map(operator_to_item))
|
||||||
|
.chain(ast::Boolean::iter().map(operator_to_item))
|
||||||
|
.chain(ast::Bits::iter().map(operator_to_item))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collection_comparison_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = common_comparison_ops();
|
||||||
|
ops.push(operator_to_item(Comparison::Has));
|
||||||
|
ops.push(operator_to_item(Comparison::NotHas));
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_comparison_ops() -> Vec<OperatorItem> {
|
||||||
|
Comparison::iter()
|
||||||
|
.filter(|op| {
|
||||||
|
!matches!(
|
||||||
|
op,
|
||||||
|
Comparison::RegexMatch
|
||||||
|
| Comparison::NotRegexMatch
|
||||||
|
| Comparison::StartsWith
|
||||||
|
| Comparison::EndsWith
|
||||||
|
| Comparison::Has
|
||||||
|
| Comparison::NotHas
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(operator_to_item)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn math_ops() -> Vec<OperatorItem> {
|
||||||
|
ast::Math::iter()
|
||||||
|
.filter(|op| !matches!(op, ast::Math::Concatenate | ast::Math::Pow))
|
||||||
|
.map(operator_to_item)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bit_ops() -> Vec<OperatorItem> {
|
||||||
|
ast::Bits::iter().map(operator_to_item).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_assignment_ops() -> Vec<OperatorItem> {
|
||||||
|
ast::Assignment::iter().map(operator_to_item).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numeric_assignment_ops() -> Vec<OperatorItem> {
|
||||||
|
ast::Assignment::iter()
|
||||||
|
.filter(|op| !matches!(op, ast::Assignment::ConcatenateAssign))
|
||||||
|
.map(operator_to_item)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn concat_assignment_ops() -> Vec<OperatorItem> {
|
||||||
|
vec![
|
||||||
|
operator_to_item(ast::Assignment::Assign),
|
||||||
|
operator_to_item(ast::Assignment::ConcatenateAssign),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_int_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = valid_float_ops();
|
||||||
|
ops.extend(bit_ops());
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_float_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = valid_value_with_unit_ops();
|
||||||
|
ops.push(operator_to_item(ast::Math::Pow));
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_string_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops: Vec<OperatorItem> = Comparison::iter().map(operator_to_item).collect();
|
||||||
|
ops.push(operator_to_item(ast::Math::Concatenate));
|
||||||
|
ops.push(OperatorItem {
|
||||||
|
symbols: "like".into(),
|
||||||
|
description: Comparison::RegexMatch
|
||||||
|
.get_message()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
ops.push(OperatorItem {
|
||||||
|
symbols: "not-like".into(),
|
||||||
|
description: Comparison::NotRegexMatch
|
||||||
|
.get_message()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
});
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_list_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = collection_comparison_ops();
|
||||||
|
ops.push(operator_to_item(ast::Math::Concatenate));
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_binary_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = number_comparison_ops();
|
||||||
|
ops.extend(bit_ops());
|
||||||
|
ops.push(operator_to_item(ast::Math::Concatenate));
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_bool_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops: Vec<OperatorItem> = ast::Boolean::iter().map(operator_to_item).collect();
|
||||||
|
ops.extend(common_comparison_ops());
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_value_with_unit_ops() -> Vec<OperatorItem> {
|
||||||
|
let mut ops = number_comparison_ops();
|
||||||
|
ops.extend(math_ops());
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ops_by_value(value: &Value, mutable: bool) -> Vec<OperatorItem> {
|
||||||
|
let mut ops = match value {
|
||||||
|
Value::Int { .. } => valid_int_ops(),
|
||||||
|
Value::Float { .. } => valid_float_ops(),
|
||||||
|
Value::String { .. } => valid_string_ops(),
|
||||||
|
Value::Binary { .. } => valid_binary_ops(),
|
||||||
|
Value::Bool { .. } => valid_bool_ops(),
|
||||||
|
Value::Date { .. } => number_comparison_ops(),
|
||||||
|
Value::Filesize { .. } | Value::Duration { .. } => valid_value_with_unit_ops(),
|
||||||
|
Value::Range { .. } | Value::Record { .. } => collection_comparison_ops(),
|
||||||
|
Value::List { .. } => valid_list_ops(),
|
||||||
|
_ => all_ops_for_immutable(),
|
||||||
|
};
|
||||||
|
if mutable {
|
||||||
|
ops.extend(match value {
|
||||||
|
Value::Int { .. }
|
||||||
|
| Value::Float { .. }
|
||||||
|
| Value::Filesize { .. }
|
||||||
|
| Value::Duration { .. } => numeric_assignment_ops(),
|
||||||
|
Value::String { .. } | Value::Binary { .. } | Value::List { .. } => {
|
||||||
|
concat_assignment_ops()
|
||||||
|
}
|
||||||
|
Value::Bool { .. }
|
||||||
|
| Value::Date { .. }
|
||||||
|
| Value::Range { .. }
|
||||||
|
| Value::Record { .. } => vec![operator_to_item(ast::Assignment::Assign)],
|
||||||
|
_ => all_assignment_ops(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_expression_mutable(expr: &Expr, working_set: &StateWorkingSet) -> bool {
|
||||||
|
let Expr::FullCellPath(path) = expr else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Expr::Var(id) = path.head.expr else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if id == ENV_VARIABLE_ID {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let var = working_set.get_variable(id);
|
||||||
|
var.mutable
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for OperatorCompletion<'_> {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut needs_assignment_ops = true;
|
||||||
|
// Complete according expression type
|
||||||
|
// TODO: type inference on self.left_hand_side to get more accurate completions
|
||||||
|
let mut possible_operations: Vec<OperatorItem> = match &self.left_hand_side.ty {
|
||||||
|
Type::Int | Type::Number => valid_int_ops(),
|
||||||
|
Type::Float => valid_float_ops(),
|
||||||
|
Type::String => valid_string_ops(),
|
||||||
|
Type::Binary => valid_binary_ops(),
|
||||||
|
Type::Bool => valid_bool_ops(),
|
||||||
|
Type::Date => number_comparison_ops(),
|
||||||
|
Type::Filesize | Type::Duration => valid_value_with_unit_ops(),
|
||||||
|
Type::Record(_) | Type::Range => collection_comparison_ops(),
|
||||||
|
Type::List(_) | Type::Table(_) => valid_list_ops(),
|
||||||
|
// Unknown type, resort to evaluated values
|
||||||
|
Type::Any => match &self.left_hand_side.expr {
|
||||||
|
Expr::FullCellPath(path) => {
|
||||||
|
// for `$ <tab>`
|
||||||
|
if matches!(path.head.expr, Expr::Garbage) {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let value =
|
||||||
|
eval_cell_path(working_set, stack, &path.head, &path.tail, path.head.span)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mutable = is_expression_mutable(&self.left_hand_side.expr, working_set);
|
||||||
|
// to avoid duplication
|
||||||
|
needs_assignment_ops = false;
|
||||||
|
ops_by_value(&value, mutable)
|
||||||
|
}
|
||||||
|
_ => all_ops_for_immutable(),
|
||||||
|
},
|
||||||
|
_ => common_comparison_ops(),
|
||||||
|
};
|
||||||
|
// If the left hand side is a variable, add assignment operators if mutable
|
||||||
|
if needs_assignment_ops && is_expression_mutable(&self.left_hand_side.expr, working_set) {
|
||||||
|
possible_operations.extend(match &self.left_hand_side.ty {
|
||||||
|
Type::Int | Type::Float | Type::Number => numeric_assignment_ops(),
|
||||||
|
Type::Filesize | Type::Duration => numeric_assignment_ops(),
|
||||||
|
Type::String | Type::Binary | Type::List(_) => concat_assignment_ops(),
|
||||||
|
Type::Any => all_assignment_ops(),
|
||||||
|
_ => vec![operator_to_item(ast::Assignment::Assign)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
for OperatorItem {
|
||||||
|
symbols,
|
||||||
|
description,
|
||||||
|
} in possible_operations
|
||||||
|
{
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: symbols.to_owned(),
|
||||||
|
description: Some(description.to_owned()),
|
||||||
|
span: reedline::Span::new(span.start - offset, span.end - offset),
|
||||||
|
append_whitespace: true,
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Operator),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span, VarId,
|
||||||
|
engine::{Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
use super::completion_options::NuMatcher;
|
||||||
|
|
||||||
|
pub struct VariableCompletion;
|
||||||
|
|
||||||
|
impl Completer for VariableCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
_stack: &Stack,
|
||||||
|
prefix: impl AsRef<str>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
options: &CompletionOptions,
|
||||||
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let mut matcher = NuMatcher::new(prefix, options);
|
||||||
|
let current_span = reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Variable completion (e.g: $en<tab> to complete $env)
|
||||||
|
let builtins = ["$nu", "$in", "$env"];
|
||||||
|
for builtin in builtins {
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: builtin.to_string(),
|
||||||
|
span: current_span,
|
||||||
|
description: Some("reserved".into()),
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Variable),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut add_candidate = |name, var_id: &VarId| {
|
||||||
|
matcher.add_semantic_suggestion(SemanticSuggestion {
|
||||||
|
suggestion: Suggestion {
|
||||||
|
value: String::from_utf8_lossy(name).to_string(),
|
||||||
|
span: current_span,
|
||||||
|
description: Some(working_set.get_variable(*var_id).ty.to_string()),
|
||||||
|
..Suggestion::default()
|
||||||
|
},
|
||||||
|
kind: Some(SuggestionKind::Variable),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: The following can be refactored (see find_commands_by_predicate() used in
|
||||||
|
// command_completions).
|
||||||
|
let mut removed_overlays = vec![];
|
||||||
|
// Working set scope vars
|
||||||
|
for scope_frame in working_set.delta.scope.iter().rev() {
|
||||||
|
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
|
||||||
|
for (name, var_id) in &overlay_frame.vars {
|
||||||
|
add_candidate(name, var_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Permanent state vars
|
||||||
|
// for scope in &self.engine_state.scope {
|
||||||
|
for overlay_frame in working_set
|
||||||
|
.permanent_state
|
||||||
|
.active_overlays(&removed_overlays)
|
||||||
|
.rev()
|
||||||
|
{
|
||||||
|
for (name, var_id) in &overlay_frame.vars {
|
||||||
|
add_candidate(name, var_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher.results()
|
||||||
|
}
|
||||||
|
}
|
368
nushell/crates/nu-cli/src/config_files.rs
Normal file
368
nushell/crates/nu-cli/src/config_files.rs
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
use crate::util::eval_source;
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
use nu_path::canonicalize_with;
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
use nu_protocol::{ParseError, PluginRegistryFile, Spanned, engine::StateWorkingSet};
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData,
|
||||||
|
engine::{EngineState, Stack},
|
||||||
|
report_shell_error,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
use nu_utils::perf;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
const PLUGIN_FILE: &str = "plugin.msgpackz";
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
|
||||||
|
use nu_protocol::{ShellError, shell_error::io::IoError};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let span = plugin_file.as_ref().map(|s| s.span);
|
||||||
|
|
||||||
|
// Check and warn + abort if this is a .nu plugin file
|
||||||
|
if plugin_file
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| Path::new(&p.item).extension())
|
||||||
|
.is_some_and(|ext| ext == "nu")
|
||||||
|
{
|
||||||
|
report_shell_error(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Wrong plugin file format".into(),
|
||||||
|
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());
|
||||||
|
perf!(
|
||||||
|
"add plugin file to engine_state",
|
||||||
|
start_time,
|
||||||
|
engine_state
|
||||||
|
.get_config()
|
||||||
|
.use_ansi_coloring
|
||||||
|
.get(engine_state)
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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_shell_error(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::Io(IoError::new_internal_with_path(
|
||||||
|
err,
|
||||||
|
"Could not open plugin registry file",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
plugin_path,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
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_shell_error(
|
||||||
|
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,
|
||||||
|
engine_state
|
||||||
|
.get_config()
|
||||||
|
.use_ansi_coloring
|
||||||
|
.get(engine_state)
|
||||||
|
);
|
||||||
|
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_shell_error(engine_state, &err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
perf!(
|
||||||
|
&format!("load plugin file {}", plugin_path.display()),
|
||||||
|
start_time,
|
||||||
|
engine_state
|
||||||
|
.get_config()
|
||||||
|
.use_ansi_coloring
|
||||||
|
.get(engine_state)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub fn add_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use nu_protocol::report_parse_error;
|
||||||
|
|
||||||
|
if let Ok(cwd) = engine_state.cwd_as_string(None) {
|
||||||
|
if let Some(plugin_file) = plugin_file {
|
||||||
|
let path = Path::new(&plugin_file.item);
|
||||||
|
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_parse_error(
|
||||||
|
&StateWorkingSet::new(engine_state),
|
||||||
|
&ParseError::FileNotFound(
|
||||||
|
path_dir.to_string_lossy().into_owned(),
|
||||||
|
plugin_file.span,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(plugin_path) = nu_path::nu_config_dir() {
|
||||||
|
// Path to store plugins signatures
|
||||||
|
let mut plugin_path =
|
||||||
|
canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
|
||||||
|
plugin_path.push(PLUGIN_FILE);
|
||||||
|
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
||||||
|
engine_state.plugin_path = Some(plugin_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_config_contents(
|
||||||
|
config_path: PathBuf,
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
) {
|
||||||
|
if config_path.exists() & config_path.is_file() {
|
||||||
|
let config_filename = config_path.to_string_lossy();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// TODO: ignore this error?
|
||||||
|
let _ = eval_source(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
&contents,
|
||||||
|
&config_filename,
|
||||||
|
PipelineData::empty(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore the current active file.
|
||||||
|
engine_state.file = prev_file;
|
||||||
|
|
||||||
|
// Merge the environment in case env vars changed in the config
|
||||||
|
if let Err(e) = engine_state.merge_env(stack) {
|
||||||
|
report_shell_error(engine_state, &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
|
||||||
|
use nu_protocol::{
|
||||||
|
PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
|
||||||
|
ShellError, shell_error::io::IoError,
|
||||||
|
};
|
||||||
|
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::nu_config_dir().and_then(|dir| 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_shell_error(
|
||||||
|
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 {
|
||||||
|
metadata: Default::default(),
|
||||||
|
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(|err| {
|
||||||
|
IoError::new_internal_with_path(
|
||||||
|
err,
|
||||||
|
"Could not create new plugin file",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
new_plugin_file_path.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(ShellError::from)
|
||||||
|
.and_then(|file| contents.write_to(file, None))
|
||||||
|
{
|
||||||
|
report_shell_error(
|
||||||
|
&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,
|
||||||
|
engine_state
|
||||||
|
.get_config()
|
||||||
|
.use_ansi_coloring
|
||||||
|
.get(&engine_state)
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
101
nushell/crates/nu-cli/src/eval_cmds.rs
Normal file
101
nushell/crates/nu-cli/src/eval_cmds.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
use log::info;
|
||||||
|
use nu_engine::eval_block;
|
||||||
|
use nu_parser::parse;
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, ShellError, Spanned, Value,
|
||||||
|
cli_error::report_compile_error,
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
report_parse_error, report_parse_warning,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::util::print_pipeline;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct EvaluateCommandsOpts {
|
||||||
|
pub table_mode: Option<Value>,
|
||||||
|
pub error_style: Option<Value>,
|
||||||
|
pub no_newline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
opts: EvaluateCommandsOpts,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let EvaluateCommandsOpts {
|
||||||
|
table_mode,
|
||||||
|
error_style,
|
||||||
|
no_newline,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
// Handle the configured error style early
|
||||||
|
if let Some(e_style) = error_style {
|
||||||
|
match e_style.coerce_str()?.parse() {
|
||||||
|
Ok(e_style) => {
|
||||||
|
Arc::make_mut(&mut engine_state.config).error_style = e_style;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(ShellError::GenericError {
|
||||||
|
error: "Invalid value for `--error-style`".into(),
|
||||||
|
msg: err.into(),
|
||||||
|
span: Some(e_style.span()),
|
||||||
|
help: None,
|
||||||
|
inner: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the source code
|
||||||
|
let (block, delta) = {
|
||||||
|
if let Some(ref t_mode) = table_mode {
|
||||||
|
Arc::make_mut(&mut engine_state.config).table.mode =
|
||||||
|
t_mode.coerce_str()?.parse().unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
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_parse_warning(&working_set, warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = working_set.parse_errors.first() {
|
||||||
|
report_parse_error(&working_set, err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
|
report_compile_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
|
|
||||||
|
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
154
nushell/crates/nu-cli/src/eval_file.rs
Normal file
154
nushell/crates/nu-cli/src/eval_file.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use crate::util::{eval_source, print_pipeline};
|
||||||
|
use log::{info, trace};
|
||||||
|
use nu_engine::eval_block;
|
||||||
|
use nu_parser::parse;
|
||||||
|
use nu_path::canonicalize_with;
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, ShellError, Span, Value,
|
||||||
|
cli_error::report_compile_error,
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
report_parse_error, report_parse_warning,
|
||||||
|
shell_error::io::*,
|
||||||
|
};
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
/// Entry point for evaluating a file.
|
||||||
|
///
|
||||||
|
/// 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(
|
||||||
|
path: String,
|
||||||
|
args: &[String],
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let cwd = engine_state.cwd_as_string(Some(stack))?;
|
||||||
|
|
||||||
|
let file_path = canonicalize_with(&path, cwd).map_err(|err| {
|
||||||
|
IoError::new_internal_with_path(
|
||||||
|
err.not_found_as(NotFound::File),
|
||||||
|
"Could not access file",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
PathBuf::from(&path),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let file_path_str = file_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| ShellError::NonUtf8Custom {
|
||||||
|
msg: format!(
|
||||||
|
"Input file name '{}' is not valid UTF8",
|
||||||
|
file_path.to_string_lossy()
|
||||||
|
),
|
||||||
|
span: Span::unknown(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let file = std::fs::read(&file_path).map_err(|err| {
|
||||||
|
IoError::new_internal_with_path(
|
||||||
|
err.not_found_as(NotFound::File),
|
||||||
|
"Could not read file",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
file_path.clone(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
engine_state.file = Some(file_path.clone());
|
||||||
|
|
||||||
|
let parent = file_path.parent().ok_or_else(|| {
|
||||||
|
IoError::new_internal_with_path(
|
||||||
|
ErrorKind::DirectoryNotFound,
|
||||||
|
"The file path does not have a parent",
|
||||||
|
nu_protocol::location!(),
|
||||||
|
file_path.clone(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
stack.add_env_var(
|
||||||
|
"FILE_PWD".to_string(),
|
||||||
|
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);
|
||||||
|
trace!("parsing file: {}", file_path_str);
|
||||||
|
let block = parse(&mut working_set, Some(file_path_str), &file, false);
|
||||||
|
|
||||||
|
if let Some(warning) = working_set.parse_warnings.first() {
|
||||||
|
report_parse_warning(&working_set, warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any parse errors were found, report the first error and exit.
|
||||||
|
if let Some(err) = working_set.parse_errors.first() {
|
||||||
|
report_parse_error(&working_set, err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
|
report_compile_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.
|
||||||
|
print_pipeline(engine_state, stack, pipeline, true)?;
|
||||||
|
|
||||||
|
// Invoke the main command with arguments.
|
||||||
|
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
||||||
|
let args = format!("main {}", args.join(" "));
|
||||||
|
eval_source(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
args.as_bytes(),
|
||||||
|
"<commandline>",
|
||||||
|
input,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
eval_source(engine_state, stack, &file, file_path_str, input, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
if exit_code != 0 {
|
||||||
|
std::process::exit(exit_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
37
nushell/crates/nu-cli/src/lib.rs
Normal file
37
nushell/crates/nu-cli/src/lib.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
mod commands;
|
||||||
|
mod completions;
|
||||||
|
mod config_files;
|
||||||
|
mod eval_cmds;
|
||||||
|
mod eval_file;
|
||||||
|
mod menus;
|
||||||
|
mod nu_highlight;
|
||||||
|
mod print;
|
||||||
|
mod prompt;
|
||||||
|
mod prompt_update;
|
||||||
|
mod reedline_config;
|
||||||
|
mod repl;
|
||||||
|
mod syntax_highlight;
|
||||||
|
mod util;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
pub use commands::add_cli_context;
|
||||||
|
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
|
||||||
|
pub use config_files::eval_config_contents;
|
||||||
|
pub use eval_cmds::{EvaluateCommandsOpts, evaluate_commands};
|
||||||
|
pub use eval_file::evaluate_file;
|
||||||
|
pub use menus::NuHelpCompleter;
|
||||||
|
pub use nu_highlight::NuHighlight;
|
||||||
|
pub use print::Print;
|
||||||
|
pub use prompt::NushellPrompt;
|
||||||
|
pub use repl::evaluate_repl;
|
||||||
|
pub use syntax_highlight::NuHighlighter;
|
||||||
|
pub use util::{eval_source, gather_parent_env_vars};
|
||||||
|
pub use validation::NuValidator;
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub use config_files::add_plugin_file;
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub use config_files::migrate_old_plugin_file;
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
pub use config_files::read_plugin_file;
|
172
nushell/crates/nu-cli/src/menus/help_completions.rs
Normal file
172
nushell/crates/nu-cli/src/menus/help_completions.rs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
use nu_engine::documentation::{HelpStyle, get_flags_section};
|
||||||
|
use nu_protocol::{Config, engine::EngineState, levenshtein_distance};
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
|
use reedline::{Completer, Suggestion};
|
||||||
|
use std::{fmt::Write, sync::Arc};
|
||||||
|
|
||||||
|
pub struct NuHelpCompleter {
|
||||||
|
engine_state: Arc<EngineState>,
|
||||||
|
config: Arc<Config>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuHelpCompleter {
|
||||||
|
pub fn new(engine_state: Arc<EngineState>, config: Arc<Config>) -> Self {
|
||||||
|
Self {
|
||||||
|
engine_state,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
let folded_line = line.to_folded_case();
|
||||||
|
|
||||||
|
let mut help_style = HelpStyle::default();
|
||||||
|
help_style.update_from_config(&self.engine_state, &self.config);
|
||||||
|
|
||||||
|
let mut commands = self
|
||||||
|
.engine_state
|
||||||
|
.get_decls_sorted(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_, decl_id)| {
|
||||||
|
let decl = self.engine_state.get_decl(decl_id);
|
||||||
|
(decl.name().to_folded_case().contains(&folded_line)
|
||||||
|
|| decl.description().to_folded_case().contains(&folded_line)
|
||||||
|
|| decl
|
||||||
|
.search_terms()
|
||||||
|
.into_iter()
|
||||||
|
.any(|term| term.to_folded_case().contains(&folded_line))
|
||||||
|
|| decl
|
||||||
|
.extra_description()
|
||||||
|
.to_folded_case()
|
||||||
|
.contains(&folded_line))
|
||||||
|
.then_some(decl)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
commands.sort_by_cached_key(|decl| levenshtein_distance(line, decl.name()));
|
||||||
|
|
||||||
|
commands
|
||||||
|
.into_iter()
|
||||||
|
.map(|decl| {
|
||||||
|
let mut long_desc = String::new();
|
||||||
|
|
||||||
|
let description = decl.description();
|
||||||
|
if !description.is_empty() {
|
||||||
|
long_desc.push_str(description);
|
||||||
|
long_desc.push_str("\r\n\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra_desc = decl.extra_description();
|
||||||
|
if !extra_desc.is_empty() {
|
||||||
|
long_desc.push_str(extra_desc);
|
||||||
|
long_desc.push_str("\r\n\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let sig = decl.signature();
|
||||||
|
let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature());
|
||||||
|
|
||||||
|
if !sig.named.is_empty() {
|
||||||
|
long_desc.push_str(&get_flags_section(&sig, &help_style, |v| {
|
||||||
|
v.to_parsable_string(", ", &self.config)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sig.required_positional.is_empty()
|
||||||
|
|| !sig.optional_positional.is_empty()
|
||||||
|
|| sig.rest_positional.is_some()
|
||||||
|
{
|
||||||
|
long_desc.push_str("\r\nParameters:\r\n");
|
||||||
|
for positional in &sig.required_positional {
|
||||||
|
let _ = write!(long_desc, " {}: {}\r\n", positional.name, positional.desc);
|
||||||
|
}
|
||||||
|
for positional in &sig.optional_positional {
|
||||||
|
let opt_suffix = if let Some(value) = &positional.default_value {
|
||||||
|
format!(
|
||||||
|
" (optional, default: {})",
|
||||||
|
&value.to_parsable_string(", ", &self.config),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(" (optional)").to_string()
|
||||||
|
};
|
||||||
|
let _ = write!(
|
||||||
|
long_desc,
|
||||||
|
" (optional) {}: {}{}\r\n",
|
||||||
|
positional.name, positional.desc, opt_suffix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rest_positional) = &sig.rest_positional {
|
||||||
|
let _ = write!(
|
||||||
|
long_desc,
|
||||||
|
" ...{}: {}\r\n",
|
||||||
|
rest_positional.name, rest_positional.desc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra: Vec<String> = decl
|
||||||
|
.examples()
|
||||||
|
.iter()
|
||||||
|
.map(|example| example.example.replace('\n', "\r\n"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Suggestion {
|
||||||
|
value: decl.name().into(),
|
||||||
|
description: Some(long_desc),
|
||||||
|
extra: Some(extra),
|
||||||
|
span: reedline::Span {
|
||||||
|
start: pos - line.len(),
|
||||||
|
end: pos,
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for NuHelpCompleter {
|
||||||
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
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 config = engine_state.get_config().clone();
|
||||||
|
let mut completer = NuHelpCompleter::new(engine_state.into(), config);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
nushell/crates/nu-cli/src/menus/menu_completions.rs
Normal file
171
nushell/crates/nu-cli/src/menus/menu_completions.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use nu_engine::eval_block;
|
||||||
|
use nu_protocol::{
|
||||||
|
BlockId, IntoPipelineData, Span, Value,
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack},
|
||||||
|
};
|
||||||
|
use reedline::{Completer, Suggestion, menu_functions::parse_selection_char};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const SELECTION_CHAR: char = '!';
|
||||||
|
|
||||||
|
pub struct NuMenuCompleter {
|
||||||
|
block_id: BlockId,
|
||||||
|
span: Span,
|
||||||
|
stack: Stack,
|
||||||
|
engine_state: Arc<EngineState>,
|
||||||
|
only_buffer_difference: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuMenuCompleter {
|
||||||
|
pub fn new(
|
||||||
|
block_id: BlockId,
|
||||||
|
span: Span,
|
||||||
|
stack: Stack,
|
||||||
|
engine_state: Arc<EngineState>,
|
||||||
|
only_buffer_difference: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
block_id,
|
||||||
|
span,
|
||||||
|
stack: stack.reset_out_dest().collect_value(),
|
||||||
|
engine_state,
|
||||||
|
only_buffer_difference,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for NuMenuCompleter {
|
||||||
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
let parsed = parse_selection_char(line, SELECTION_CHAR);
|
||||||
|
|
||||||
|
let block = self.engine_state.get_block(self.block_id);
|
||||||
|
|
||||||
|
if let Some(buffer) = block.signature.get_positional(0) {
|
||||||
|
if let Some(buffer_id) = &buffer.var_id {
|
||||||
|
let line_buffer = Value::string(parsed.remainder, self.span);
|
||||||
|
self.stack.add_var(*buffer_id, line_buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(position) = block.signature.get_positional(1) {
|
||||||
|
if let Some(position_id) = &position.var_id {
|
||||||
|
let line_buffer = Value::int(pos as i64, self.span);
|
||||||
|
self.stack.add_var(*position_id, line_buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = Value::nothing(self.span).into_pipeline_data();
|
||||||
|
|
||||||
|
let res = eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, block, input);
|
||||||
|
|
||||||
|
if let Ok(values) = res.and_then(|data| data.into_value(self.span)) {
|
||||||
|
convert_to_suggestions(values, line, pos, self.only_buffer_difference)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_to_suggestions(
|
||||||
|
value: Value,
|
||||||
|
line: &str,
|
||||||
|
pos: usize,
|
||||||
|
only_buffer_difference: bool,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
match value {
|
||||||
|
Value::Record { val, .. } => {
|
||||||
|
let text = val
|
||||||
|
.get("value")
|
||||||
|
.and_then(|val| val.coerce_string().ok())
|
||||||
|
.unwrap_or_else(|| "No value key".to_string());
|
||||||
|
|
||||||
|
let description = val
|
||||||
|
.get("description")
|
||||||
|
.and_then(|val| val.coerce_string().ok());
|
||||||
|
|
||||||
|
let span = match val.get("span") {
|
||||||
|
Some(Value::Record { val: span, .. }) => {
|
||||||
|
let start = span.get("start").and_then(|val| val.as_int().ok());
|
||||||
|
let end = span.get("end").and_then(|val| val.as_int().ok());
|
||||||
|
match (start, end) {
|
||||||
|
(Some(start), Some(end)) => {
|
||||||
|
let start = start.min(end);
|
||||||
|
reedline::Span {
|
||||||
|
start: start as usize,
|
||||||
|
end: end as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => reedline::Span {
|
||||||
|
start: if only_buffer_difference {
|
||||||
|
pos - line.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
end: if only_buffer_difference {
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
line.len()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => reedline::Span {
|
||||||
|
start: if only_buffer_difference {
|
||||||
|
pos - line.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
end: if only_buffer_difference {
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
line.len()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let extra = match val.get("extra") {
|
||||||
|
Some(Value::List { vals, .. }) => {
|
||||||
|
let extra: Vec<String> = vals
|
||||||
|
.iter()
|
||||||
|
.filter_map(|extra| match extra {
|
||||||
|
Value::String { val, .. } => Some(val.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(extra)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![Suggestion {
|
||||||
|
value: text,
|
||||||
|
description,
|
||||||
|
extra,
|
||||||
|
span,
|
||||||
|
..Suggestion::default()
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
Value::List { vals, .. } => vals
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|val| convert_to_suggestions(val, line, pos, only_buffer_difference))
|
||||||
|
.collect(),
|
||||||
|
_ => vec![Suggestion {
|
||||||
|
value: format!("Not a record: {value:?}"),
|
||||||
|
span: reedline::Span {
|
||||||
|
start: if only_buffer_difference {
|
||||||
|
pos - line.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
end: if only_buffer_difference {
|
||||||
|
pos
|
||||||
|
} else {
|
||||||
|
line.len()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
..Suggestion::default()
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
5
nushell/crates/nu-cli/src/menus/mod.rs
Normal file
5
nushell/crates/nu-cli/src/menus/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod help_completions;
|
||||||
|
mod menu_completions;
|
||||||
|
|
||||||
|
pub use help_completions::NuHelpCompleter;
|
||||||
|
pub use menu_completions::NuMenuCompleter;
|
76
nushell/crates/nu-cli/src/nu_highlight.rs
Normal file
76
nushell/crates/nu-cli/src/nu_highlight.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use reedline::{Highlighter, StyledText};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NuHighlight;
|
||||||
|
|
||||||
|
impl Command for NuHighlight {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"nu-highlight"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("nu-highlight")
|
||||||
|
.category(Category::Strings)
|
||||||
|
.input_output_types(vec![(Type::String, Type::String)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Syntax highlight the input string."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["syntax", "color", "convert"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
let signals = engine_state.signals();
|
||||||
|
|
||||||
|
let highlighter = crate::NuHighlighter {
|
||||||
|
engine_state: Arc::new(engine_state.clone()),
|
||||||
|
stack: Arc::new(stack.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
input.map(
|
||||||
|
move |x| match x.coerce_into_string() {
|
||||||
|
Ok(line) => {
|
||||||
|
let highlights = highlighter.highlight(&line, line.len());
|
||||||
|
Value::string(highlights.render_simple(), head)
|
||||||
|
}
|
||||||
|
Err(err) => Value::error(err, head),
|
||||||
|
},
|
||||||
|
signals,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
description: "Describe the type of a string",
|
||||||
|
example: "'let x = 3' | nu-highlight",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
111
nushell/crates/nu-cli/src/print.rs
Normal file
111
nushell/crates/nu-cli/src/print.rs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
use nu_engine::command_prelude::*;
|
||||||
|
use nu_protocol::ByteStreamSource;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Print;
|
||||||
|
|
||||||
|
impl Command for Print {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"print"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
Signature::build("print")
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::Nothing, Type::Nothing),
|
||||||
|
(Type::Any, Type::Nothing),
|
||||||
|
])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
.rest("rest", SyntaxShape::Any, "the values to print")
|
||||||
|
.switch(
|
||||||
|
"no-newline",
|
||||||
|
"print without inserting a newline for the line ending",
|
||||||
|
Some('n'),
|
||||||
|
)
|
||||||
|
.switch("stderr", "print to stderr instead of stdout", Some('e'))
|
||||||
|
.switch(
|
||||||
|
"raw",
|
||||||
|
"print without formatting (including binary data)",
|
||||||
|
Some('r'),
|
||||||
|
)
|
||||||
|
.category(Category::Strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Print the given values to stdout."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_description(&self) -> &str {
|
||||||
|
r#"Unlike `echo`, this command does not return any value (`print | describe` will return "nothing").
|
||||||
|
Since this command has no output, there is no point in piping it with other commands.
|
||||||
|
|
||||||
|
`print` may be used inside blocks of code (e.g.: hooks) to display text during execution without interfering with the pipeline."#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["display"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
mut input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
let args: Vec<Value> = call.rest(engine_state, stack, 0)?;
|
||||||
|
let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
|
||||||
|
let to_stderr = call.has_flag(engine_state, stack, "stderr")?;
|
||||||
|
let raw = call.has_flag(engine_state, stack, "raw")?;
|
||||||
|
|
||||||
|
// This will allow for easy printing of pipelines as well
|
||||||
|
if !args.is_empty() {
|
||||||
|
for arg in args {
|
||||||
|
if raw {
|
||||||
|
arg.into_pipeline_data()
|
||||||
|
.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
|
} else {
|
||||||
|
arg.into_pipeline_data().print_table(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
no_newline,
|
||||||
|
to_stderr,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !input.is_nothing() {
|
||||||
|
if let PipelineData::ByteStream(stream, _) = &mut input {
|
||||||
|
if let ByteStreamSource::Child(child) = stream.source_mut() {
|
||||||
|
child.ignore_error(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw {
|
||||||
|
input.print_raw(engine_state, no_newline, to_stderr)?;
|
||||||
|
} else {
|
||||||
|
input.print_table(engine_state, stack, no_newline, to_stderr)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PipelineData::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Print 'hello world'",
|
||||||
|
example: r#"print "hello world""#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print the sum of 2 and 3",
|
||||||
|
example: r#"print (2 + 3)"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Print 'ABC' from binary data",
|
||||||
|
example: r#"0x[41 42 43] | print --raw"#,
|
||||||
|
result: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
212
nushell/crates/nu-cli/src/prompt.rs
Normal file
212
nushell/crates/nu-cli/src/prompt.rs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
use crate::prompt_update::{
|
||||||
|
POST_PROMPT_MARKER, PRE_PROMPT_MARKER, VSCODE_POST_PROMPT_MARKER, VSCODE_PRE_PROMPT_MARKER,
|
||||||
|
};
|
||||||
|
use nu_protocol::engine::{EngineState, Stack};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use nu_utils::enable_vt_processing;
|
||||||
|
use reedline::{
|
||||||
|
DefaultPrompt, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus,
|
||||||
|
PromptViMode,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// Nushell prompt definition
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NushellPrompt {
|
||||||
|
shell_integration_osc133: bool,
|
||||||
|
shell_integration_osc633: bool,
|
||||||
|
left_prompt_string: Option<String>,
|
||||||
|
right_prompt_string: Option<String>,
|
||||||
|
default_prompt_indicator: Option<String>,
|
||||||
|
default_vi_insert_prompt_indicator: Option<String>,
|
||||||
|
default_vi_normal_prompt_indicator: Option<String>,
|
||||||
|
default_multiline_indicator: Option<String>,
|
||||||
|
render_right_prompt_on_last_line: bool,
|
||||||
|
engine_state: EngineState,
|
||||||
|
stack: Stack,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NushellPrompt {
|
||||||
|
pub fn new(
|
||||||
|
shell_integration_osc133: bool,
|
||||||
|
shell_integration_osc633: bool,
|
||||||
|
engine_state: EngineState,
|
||||||
|
stack: Stack,
|
||||||
|
) -> NushellPrompt {
|
||||||
|
NushellPrompt {
|
||||||
|
shell_integration_osc133,
|
||||||
|
shell_integration_osc633,
|
||||||
|
left_prompt_string: None,
|
||||||
|
right_prompt_string: None,
|
||||||
|
default_prompt_indicator: None,
|
||||||
|
default_vi_insert_prompt_indicator: None,
|
||||||
|
default_vi_normal_prompt_indicator: None,
|
||||||
|
default_multiline_indicator: None,
|
||||||
|
render_right_prompt_on_last_line: false,
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_left(&mut self, prompt_string: Option<String>) {
|
||||||
|
self.left_prompt_string = prompt_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_right(
|
||||||
|
&mut self,
|
||||||
|
prompt_string: Option<String>,
|
||||||
|
render_right_prompt_on_last_line: bool,
|
||||||
|
) {
|
||||||
|
self.right_prompt_string = prompt_string;
|
||||||
|
self.render_right_prompt_on_last_line = render_right_prompt_on_last_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_indicator(&mut self, prompt_indicator_string: Option<String>) {
|
||||||
|
self.default_prompt_indicator = prompt_indicator_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_vi_insert(&mut self, prompt_vi_insert_string: Option<String>) {
|
||||||
|
self.default_vi_insert_prompt_indicator = prompt_vi_insert_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_vi_normal(&mut self, prompt_vi_normal_string: Option<String>) {
|
||||||
|
self.default_vi_normal_prompt_indicator = prompt_vi_normal_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: Option<String>) {
|
||||||
|
self.default_multiline_indicator = prompt_multiline_indicator_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_all_prompt_strings(
|
||||||
|
&mut self,
|
||||||
|
left_prompt_string: Option<String>,
|
||||||
|
right_prompt_string: Option<String>,
|
||||||
|
prompt_indicator_string: Option<String>,
|
||||||
|
prompt_multiline_indicator_string: Option<String>,
|
||||||
|
prompt_vi: (Option<String>, Option<String>),
|
||||||
|
render_right_prompt_on_last_line: bool,
|
||||||
|
) {
|
||||||
|
let (prompt_vi_insert_string, prompt_vi_normal_string) = prompt_vi;
|
||||||
|
|
||||||
|
self.left_prompt_string = left_prompt_string;
|
||||||
|
self.right_prompt_string = right_prompt_string;
|
||||||
|
self.default_prompt_indicator = prompt_indicator_string;
|
||||||
|
self.default_multiline_indicator = prompt_multiline_indicator_string;
|
||||||
|
|
||||||
|
self.default_vi_insert_prompt_indicator = prompt_vi_insert_string;
|
||||||
|
self.default_vi_normal_prompt_indicator = prompt_vi_normal_string;
|
||||||
|
|
||||||
|
self.render_right_prompt_on_last_line = render_right_prompt_on_last_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_wrapped_custom_string(&self, str: String) -> String {
|
||||||
|
format!("({str})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Prompt for NushellPrompt {
|
||||||
|
fn render_prompt_left(&self) -> Cow<str> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = enable_vt_processing();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prompt_string) = &self.left_prompt_string {
|
||||||
|
prompt_string.replace('\n', "\r\n").into()
|
||||||
|
} else {
|
||||||
|
let default = DefaultPrompt::default();
|
||||||
|
let prompt = default
|
||||||
|
.render_prompt_left()
|
||||||
|
.to_string()
|
||||||
|
.replace('\n', "\r\n");
|
||||||
|
|
||||||
|
if self.shell_integration_osc633 {
|
||||||
|
if self
|
||||||
|
.stack
|
||||||
|
.get_env_var(&self.engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_prompt_right(&self) -> Cow<str> {
|
||||||
|
if let Some(prompt_string) = &self.right_prompt_string {
|
||||||
|
prompt_string.replace('\n', "\r\n").into()
|
||||||
|
} else {
|
||||||
|
let default = DefaultPrompt::default();
|
||||||
|
default
|
||||||
|
.render_prompt_right()
|
||||||
|
.to_string()
|
||||||
|
.replace('\n', "\r\n")
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
|
||||||
|
match edit_mode {
|
||||||
|
PromptEditMode::Default => match &self.default_prompt_indicator {
|
||||||
|
Some(indicator) => indicator,
|
||||||
|
None => "> ",
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
PromptEditMode::Emacs => match &self.default_prompt_indicator {
|
||||||
|
Some(indicator) => indicator,
|
||||||
|
None => "> ",
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
PromptEditMode::Vi(vi_mode) => match vi_mode {
|
||||||
|
PromptViMode::Normal => match &self.default_vi_normal_prompt_indicator {
|
||||||
|
Some(indicator) => indicator,
|
||||||
|
None => "> ",
|
||||||
|
},
|
||||||
|
PromptViMode::Insert => match &self.default_vi_insert_prompt_indicator {
|
||||||
|
Some(indicator) => indicator,
|
||||||
|
None => ": ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
PromptEditMode::Custom(str) => self.default_wrapped_custom_string(str).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
|
||||||
|
match &self.default_multiline_indicator {
|
||||||
|
Some(indicator) => indicator,
|
||||||
|
None => "::: ",
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_prompt_history_search_indicator(
|
||||||
|
&self,
|
||||||
|
history_search: PromptHistorySearch,
|
||||||
|
) -> Cow<str> {
|
||||||
|
let prefix = match history_search.status {
|
||||||
|
PromptHistorySearchStatus::Passing => "",
|
||||||
|
PromptHistorySearchStatus::Failing => "failing ",
|
||||||
|
};
|
||||||
|
|
||||||
|
Cow::Owned(format!(
|
||||||
|
"({}reverse-search: {})",
|
||||||
|
prefix, history_search.term
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn right_prompt_on_last_line(&self) -> bool {
|
||||||
|
self.render_right_prompt_on_last_line
|
||||||
|
}
|
||||||
|
}
|
213
nushell/crates/nu-cli/src/prompt_update.rs
Normal file
213
nushell/crates/nu-cli/src/prompt_update.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use crate::NushellPrompt;
|
||||||
|
use log::{trace, warn};
|
||||||
|
use nu_engine::ClosureEvalOnce;
|
||||||
|
use nu_protocol::{
|
||||||
|
Config, PipelineData, Value,
|
||||||
|
engine::{EngineState, Stack},
|
||||||
|
report_shell_error,
|
||||||
|
};
|
||||||
|
use reedline::Prompt;
|
||||||
|
|
||||||
|
// Name of environment variable where the prompt could be stored
|
||||||
|
pub(crate) const PROMPT_COMMAND: &str = "PROMPT_COMMAND";
|
||||||
|
pub(crate) const PROMPT_COMMAND_RIGHT: &str = "PROMPT_COMMAND_RIGHT";
|
||||||
|
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_NORMAL: &str = "PROMPT_INDICATOR_VI_NORMAL";
|
||||||
|
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:
|
||||||
|
// <133 A><prompt><133 B><command><133 C><command output>
|
||||||
|
pub(crate) const PRE_PROMPT_MARKER: &str = "\x1b]133;A\x1b\\";
|
||||||
|
pub(crate) const POST_PROMPT_MARKER: &str = "\x1b]133;B\x1b\\";
|
||||||
|
pub(crate) const PRE_EXECUTION_MARKER: &str = "\x1b]133;C\x1b\\";
|
||||||
|
pub(crate) const POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]133;D;";
|
||||||
|
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\\";
|
||||||
|
pub(crate) const VSCODE_PRE_EXECUTION_MARKER: &str = "\x1b]633;C\x1b\\";
|
||||||
|
//"\x1b]633;D;{}\x1b\\"
|
||||||
|
pub(crate) const VSCODE_POST_EXECUTION_MARKER_PREFIX: &str = "\x1b]633;D;";
|
||||||
|
pub(crate) const VSCODE_POST_EXECUTION_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
|
//"\x1b]633;E;{}\x1b\\"
|
||||||
|
pub(crate) const VSCODE_COMMANDLINE_MARKER_PREFIX: &str = "\x1b]633;E;";
|
||||||
|
pub(crate) const VSCODE_COMMANDLINE_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
|
// "\x1b]633;P;Cwd={}\x1b\\"
|
||||||
|
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_PREFIX: &str = "\x1b]633;P;Cwd=";
|
||||||
|
pub(crate) const VSCODE_CWD_PROPERTY_MARKER_SUFFIX: &str = "\x1b\\";
|
||||||
|
|
||||||
|
pub(crate) const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
||||||
|
|
||||||
|
fn get_prompt_string(
|
||||||
|
prompt: &str,
|
||||||
|
config: &Config,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
) -> Option<String> {
|
||||||
|
stack
|
||||||
|
.get_env_var(engine_state, prompt)
|
||||||
|
.and_then(|v| match v {
|
||||||
|
Value::Closure { val, .. } => {
|
||||||
|
let result = ClosureEvalOnce::new(engine_state, stack, val.as_ref().clone())
|
||||||
|
.run_with_input(PipelineData::Empty);
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"get_prompt_string (block) {}:{}:{}",
|
||||||
|
file!(),
|
||||||
|
line!(),
|
||||||
|
column!()
|
||||||
|
);
|
||||||
|
|
||||||
|
result
|
||||||
|
.map_err(|err| {
|
||||||
|
report_shell_error(engine_state, &err);
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
Value::String { .. } => Some(PipelineData::Value(v.clone(), None)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.and_then(|pipeline_data| {
|
||||||
|
let output = pipeline_data.collect_string("", config).ok();
|
||||||
|
let ansi_output = output.map(|mut x| {
|
||||||
|
// Always reset the color at the start of the right prompt
|
||||||
|
// to ensure there is no ansi bleed over
|
||||||
|
if x.is_empty() && prompt == PROMPT_COMMAND_RIGHT {
|
||||||
|
x.insert_str(0, "\x1b[0m")
|
||||||
|
};
|
||||||
|
|
||||||
|
x
|
||||||
|
});
|
||||||
|
// Let's keep this for debugging purposes with nu --log-level warn
|
||||||
|
warn!("{}:{}:{} {:?}", file!(), line!(), column!(), ansi_output);
|
||||||
|
|
||||||
|
ansi_output
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_prompt(
|
||||||
|
config: &Config,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
nu_prompt: &mut NushellPrompt,
|
||||||
|
) {
|
||||||
|
let configured_left_prompt_string =
|
||||||
|
match get_prompt_string(PROMPT_COMMAND, config, engine_state, stack) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now that we have the prompt string lets ansify it.
|
||||||
|
// <133 A><prompt><133 B><command><133 C><command output>
|
||||||
|
let left_prompt_string = if config.shell_integration.osc633 {
|
||||||
|
if stack
|
||||||
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
||||||
|
.and_then(|v| v.as_str().ok())
|
||||||
|
== Some("vscode")
|
||||||
|
{
|
||||||
|
// We're in vscode and we have osc633 enabled
|
||||||
|
Some(format!(
|
||||||
|
"{VSCODE_PRE_PROMPT_MARKER}{configured_left_prompt_string}{VSCODE_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 {
|
||||||
|
configured_left_prompt_string.into()
|
||||||
|
}
|
||||||
|
} else if config.shell_integration.osc133 {
|
||||||
|
Some(format!(
|
||||||
|
"{PRE_PROMPT_MARKER}{configured_left_prompt_string}{POST_PROMPT_MARKER}"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
configured_left_prompt_string.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);
|
||||||
|
|
||||||
|
let prompt_indicator_string = get_prompt_string(PROMPT_INDICATOR, config, engine_state, stack);
|
||||||
|
|
||||||
|
let prompt_multiline_string =
|
||||||
|
get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, stack);
|
||||||
|
|
||||||
|
let prompt_vi_insert_string =
|
||||||
|
get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, stack);
|
||||||
|
|
||||||
|
let prompt_vi_normal_string =
|
||||||
|
get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, stack);
|
||||||
|
|
||||||
|
// apply the other indicators
|
||||||
|
nu_prompt.update_all_prompt_strings(
|
||||||
|
left_prompt_string,
|
||||||
|
right_prompt_string,
|
||||||
|
prompt_indicator_string,
|
||||||
|
prompt_multiline_string,
|
||||||
|
(prompt_vi_insert_string, prompt_vi_normal_string),
|
||||||
|
config.render_right_prompt_on_last_line,
|
||||||
|
);
|
||||||
|
trace!("update_prompt {}:{}:{}", file!(), line!(), column!());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
1514
nushell/crates/nu-cli/src/reedline_config.rs
Normal file
1514
nushell/crates/nu-cli/src/reedline_config.rs
Normal file
File diff suppressed because it is too large
Load Diff
1651
nushell/crates/nu-cli/src/repl.rs
Normal file
1651
nushell/crates/nu-cli/src/repl.rs
Normal file
File diff suppressed because it is too large
Load Diff
526
nushell/crates/nu-cli/src/syntax_highlight.rs
Normal file
526
nushell/crates/nu-cli/src/syntax_highlight.rs
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
use log::trace;
|
||||||
|
use nu_ansi_term::Style;
|
||||||
|
use nu_color_config::{get_matching_brackets_style, get_shape_color};
|
||||||
|
use nu_engine::env;
|
||||||
|
use nu_parser::{FlatShape, flatten_block, parse};
|
||||||
|
use nu_protocol::{
|
||||||
|
Span,
|
||||||
|
ast::{Block, Expr, Expression, PipelineRedirection, RecordItem},
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::{Highlighter, StyledText};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct NuHighlighter {
|
||||||
|
pub engine_state: Arc<EngineState>,
|
||||||
|
pub stack: Arc<Stack>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Highlighter for NuHighlighter {
|
||||||
|
fn highlight(&self, line: &str, cursor: usize) -> StyledText {
|
||||||
|
trace!("highlighting: {}", line);
|
||||||
|
|
||||||
|
let config = self.stack.get_config(&self.engine_state);
|
||||||
|
let highlight_resolved_externals = config.highlight_resolved_externals;
|
||||||
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
|
let block = parse(&mut working_set, None, line.as_bytes(), false);
|
||||||
|
let (shapes, global_span_offset) = {
|
||||||
|
let mut 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())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output = StyledText::default();
|
||||||
|
let mut last_seen_span = global_span_offset;
|
||||||
|
|
||||||
|
let global_cursor_offset = cursor + global_span_offset;
|
||||||
|
let matching_brackets_pos = find_matching_brackets(
|
||||||
|
line,
|
||||||
|
&working_set,
|
||||||
|
&block,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
for shape in &shapes {
|
||||||
|
if shape.0.end <= last_seen_span
|
||||||
|
|| last_seen_span < global_span_offset
|
||||||
|
|| shape.0.start < global_span_offset
|
||||||
|
{
|
||||||
|
// We've already output something for this span
|
||||||
|
// so just skip this one
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if shape.0.start > last_seen_span {
|
||||||
|
let gap = line
|
||||||
|
[(last_seen_span - global_span_offset)..(shape.0.start - global_span_offset)]
|
||||||
|
.to_string();
|
||||||
|
output.push((Style::new(), gap));
|
||||||
|
}
|
||||||
|
let next_token = line
|
||||||
|
[(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)]
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut add_colored_token = |shape: &FlatShape, text: String| {
|
||||||
|
output.push((get_shape_color(shape.as_str(), &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::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(
|
||||||
|
line,
|
||||||
|
span,
|
||||||
|
&matching_brackets_pos,
|
||||||
|
global_span_offset,
|
||||||
|
);
|
||||||
|
for (part, highlight) in spans {
|
||||||
|
let start = part.start - span.start;
|
||||||
|
let end = part.end - span.start;
|
||||||
|
let text = next_token[start..end].to_string();
|
||||||
|
let mut style = get_shape_color(shape.as_str(), &config);
|
||||||
|
if highlight {
|
||||||
|
style = get_matching_brackets_style(style, &config);
|
||||||
|
}
|
||||||
|
output.push((style, text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatShape::Filepath => add_colored_token(&shape.1, next_token),
|
||||||
|
FlatShape::Directory => add_colored_token(&shape.1, next_token),
|
||||||
|
FlatShape::GlobInterpolation => add_colored_token(&shape.1, 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::Redirection => add_colored_token(&shape.1, next_token),
|
||||||
|
FlatShape::Custom(..) => add_colored_token(&shape.1, next_token),
|
||||||
|
FlatShape::MatchPattern => add_colored_token(&shape.1, next_token),
|
||||||
|
}
|
||||||
|
last_seen_span = shape.0.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainder = line[(last_seen_span - global_span_offset)..].to_string();
|
||||||
|
if !remainder.is_empty() {
|
||||||
|
output.push((Style::new(), remainder));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_span_by_highlight_positions(
|
||||||
|
line: &str,
|
||||||
|
span: Span,
|
||||||
|
highlight_positions: &[usize],
|
||||||
|
global_span_offset: usize,
|
||||||
|
) -> Vec<(Span, bool)> {
|
||||||
|
let mut start = span.start;
|
||||||
|
let mut result: Vec<(Span, bool)> = Vec::new();
|
||||||
|
for pos in highlight_positions {
|
||||||
|
if start <= *pos && pos < &span.end {
|
||||||
|
if start < *pos {
|
||||||
|
result.push((Span::new(start, *pos), false));
|
||||||
|
}
|
||||||
|
let span_str = &line[pos - global_span_offset..span.end - global_span_offset];
|
||||||
|
let end = span_str
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map(|c| pos + get_char_length(c))
|
||||||
|
.unwrap_or(pos + 1);
|
||||||
|
result.push((Span::new(*pos, end), true));
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < span.end {
|
||||||
|
result.push((Span::new(start, span.end), false));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_brackets(
|
||||||
|
line: &str,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
block: &Block,
|
||||||
|
global_span_offset: usize,
|
||||||
|
global_cursor_offset: usize,
|
||||||
|
) -> Vec<usize> {
|
||||||
|
const BRACKETS: &str = "{}[]()";
|
||||||
|
|
||||||
|
// calculate first bracket position
|
||||||
|
let global_end_offset = line.len() + global_span_offset;
|
||||||
|
let global_bracket_pos =
|
||||||
|
if global_cursor_offset == global_end_offset && global_end_offset > global_span_offset {
|
||||||
|
// cursor is at the end of a non-empty string -- find block end at the previous position
|
||||||
|
if let Some(last_char) = line.chars().last() {
|
||||||
|
global_cursor_offset - get_char_length(last_char)
|
||||||
|
} else {
|
||||||
|
global_cursor_offset
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cursor is in the middle of a string -- find block end at the current position
|
||||||
|
global_cursor_offset
|
||||||
|
};
|
||||||
|
|
||||||
|
// check that position contains bracket
|
||||||
|
let match_idx = global_bracket_pos - global_span_offset;
|
||||||
|
if match_idx >= line.len()
|
||||||
|
|| !BRACKETS.contains(get_char_at_index(line, match_idx).unwrap_or_default())
|
||||||
|
{
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// find matching bracket by finding matching block end
|
||||||
|
let matching_block_end = find_matching_block_end_in_block(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
block,
|
||||||
|
global_span_offset,
|
||||||
|
global_bracket_pos,
|
||||||
|
);
|
||||||
|
if let Some(pos) = matching_block_end {
|
||||||
|
let matching_idx = pos - global_span_offset;
|
||||||
|
if BRACKETS.contains(get_char_at_index(line, matching_idx).unwrap_or_default()) {
|
||||||
|
return if global_bracket_pos < pos {
|
||||||
|
vec![global_bracket_pos, pos]
|
||||||
|
} else {
|
||||||
|
vec![pos, global_bracket_pos]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_block_end_in_block(
|
||||||
|
line: &str,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
block: &Block,
|
||||||
|
global_span_offset: usize,
|
||||||
|
global_cursor_offset: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
for p in &block.pipelines {
|
||||||
|
for e in &p.elements {
|
||||||
|
if e.expr.span.contains(global_cursor_offset) {
|
||||||
|
if let Some(pos) = find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
&e.expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
) {
|
||||||
|
return Some(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(redirection) = e.redirection.as_ref() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_matching_block_end_in_expr(
|
||||||
|
line: &str,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
expression: &Expression,
|
||||||
|
global_span_offset: usize,
|
||||||
|
global_cursor_offset: usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if expression.span.contains(global_cursor_offset) && expression.span.start >= global_span_offset
|
||||||
|
{
|
||||||
|
let expr_first = expression.span.start;
|
||||||
|
let span_str = &line
|
||||||
|
[expression.span.start - global_span_offset..expression.span.end - global_span_offset];
|
||||||
|
let expr_last = span_str
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.map(|c| expression.span.end - get_char_length(c))
|
||||||
|
.unwrap_or(expression.span.start);
|
||||||
|
|
||||||
|
return match &expression.expr {
|
||||||
|
// TODO: Can't these be handled with an `_ => None` branch? Refactor
|
||||||
|
Expr::Bool(_) => None,
|
||||||
|
Expr::Int(_) => None,
|
||||||
|
Expr::Float(_) => None,
|
||||||
|
Expr::Binary(_) => None,
|
||||||
|
Expr::Range(..) => None,
|
||||||
|
Expr::Var(_) => None,
|
||||||
|
Expr::VarDecl(_) => None,
|
||||||
|
Expr::ExternalCall(..) => None,
|
||||||
|
Expr::Operator(_) => None,
|
||||||
|
Expr::UnaryNot(_) => None,
|
||||||
|
Expr::Keyword(..) => None,
|
||||||
|
Expr::ValueWithUnit(..) => None,
|
||||||
|
Expr::DateTime(_) => None,
|
||||||
|
Expr::Filepath(_, _) => None,
|
||||||
|
Expr::Directory(_, _) => None,
|
||||||
|
Expr::GlobPattern(_, _) => None,
|
||||||
|
Expr::String(_) => None,
|
||||||
|
Expr::RawString(_) => None,
|
||||||
|
Expr::CellPath(_) => None,
|
||||||
|
Expr::ImportPattern(_) => None,
|
||||||
|
Expr::Overlay(_) => None,
|
||||||
|
Expr::Signature(_) => None,
|
||||||
|
Expr::MatchBlock(_) => None,
|
||||||
|
Expr::Nothing => None,
|
||||||
|
Expr::Garbage => None,
|
||||||
|
|
||||||
|
Expr::AttributeBlock(ab) => ab
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.find_map(|attr| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
&attr.expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
&ab.item,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
|
Expr::Table(table) => {
|
||||||
|
if expr_last == global_cursor_offset {
|
||||||
|
// cursor is at table end
|
||||||
|
Some(expr_first)
|
||||||
|
} else if expr_first == global_cursor_offset {
|
||||||
|
// cursor is at table start
|
||||||
|
Some(expr_last)
|
||||||
|
} else {
|
||||||
|
// cursor is inside table
|
||||||
|
table
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.chain(table.rows.iter().flat_map(AsRef::as_ref))
|
||||||
|
.find_map(|expr| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::Record(exprs) => {
|
||||||
|
if expr_last == global_cursor_offset {
|
||||||
|
// cursor is at record end
|
||||||
|
Some(expr_first)
|
||||||
|
} else if expr_first == global_cursor_offset {
|
||||||
|
// cursor is at record start
|
||||||
|
Some(expr_last)
|
||||||
|
} else {
|
||||||
|
// cursor is inside record
|
||||||
|
exprs.iter().find_map(|expr| match expr {
|
||||||
|
RecordItem::Pair(k, v) => find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
k,
|
||||||
|
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| {
|
||||||
|
arg.expr().and_then(|expr| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
Expr::FullCellPath(b) => find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
&b.head,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
),
|
||||||
|
|
||||||
|
Expr::BinaryOp(lhs, op, rhs) => [lhs, op, rhs].into_iter().find_map(|expr| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
|
Expr::Collect(_, expr) => find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
),
|
||||||
|
|
||||||
|
Expr::Block(block_id)
|
||||||
|
| Expr::Closure(block_id)
|
||||||
|
| Expr::RowCondition(block_id)
|
||||||
|
| Expr::Subexpression(block_id) => {
|
||||||
|
if expr_last == global_cursor_offset {
|
||||||
|
// cursor is at block end
|
||||||
|
Some(expr_first)
|
||||||
|
} else if expr_first == global_cursor_offset {
|
||||||
|
// cursor is at block start
|
||||||
|
Some(expr_last)
|
||||||
|
} else {
|
||||||
|
// cursor is inside block
|
||||||
|
let nested_block = working_set.get_block(*block_id);
|
||||||
|
find_matching_block_end_in_block(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
nested_block,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
|
||||||
|
exprs.iter().find_map(|expr| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
expr,
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Expr::List(list) => {
|
||||||
|
if expr_last == global_cursor_offset {
|
||||||
|
// cursor is at list end
|
||||||
|
Some(expr_first)
|
||||||
|
} else if expr_first == global_cursor_offset {
|
||||||
|
// cursor is at list start
|
||||||
|
Some(expr_last)
|
||||||
|
} else {
|
||||||
|
list.iter().find_map(|item| {
|
||||||
|
find_matching_block_end_in_expr(
|
||||||
|
line,
|
||||||
|
working_set,
|
||||||
|
item.expr(),
|
||||||
|
global_span_offset,
|
||||||
|
global_cursor_offset,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_char_at_index(s: &str, index: usize) -> Option<char> {
|
||||||
|
s[index..].chars().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_char_length(c: char) -> usize {
|
||||||
|
c.to_string().len()
|
||||||
|
}
|
352
nushell/crates/nu-cli/src/util.rs
Normal file
352
nushell/crates/nu-cli/src/util.rs
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
#![allow(clippy::byte_char_slices)]
|
||||||
|
|
||||||
|
use nu_cmd_base::hook::eval_hook;
|
||||||
|
use nu_engine::{eval_block, eval_block_with_early_return};
|
||||||
|
use nu_parser::{Token, TokenContents, lex, parse, unescape_unquote_string};
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, ShellError, Span, Value,
|
||||||
|
cli_error::report_compile_error,
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
report_parse_error, report_parse_warning, report_shell_error,
|
||||||
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use nu_utils::enable_vt_processing;
|
||||||
|
use nu_utils::{escape_quote_string, perf};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// This will collect environment variables from std::env and adds them to a stack.
|
||||||
|
//
|
||||||
|
// In order to ensure the values have spans, it first creates a dummy file, writes the collected
|
||||||
|
// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env'
|
||||||
|
// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done.
|
||||||
|
//
|
||||||
|
// The "PWD" env value will be forced to `init_cwd`.
|
||||||
|
// The reason to use `init_cwd`:
|
||||||
|
//
|
||||||
|
// While gathering parent env vars, the parent `PWD` may not be the same as `current working directory`.
|
||||||
|
// Consider to the following command as the case (assume we execute command inside `/tmp`):
|
||||||
|
//
|
||||||
|
// tmux split-window -v -c "#{pane_current_path}"
|
||||||
|
//
|
||||||
|
// Here nu execute external command `tmux`, and tmux starts a new `nushell`, with `init_cwd` value "#{pane_current_path}".
|
||||||
|
// But at the same time `PWD` still remains to be `/tmp`.
|
||||||
|
//
|
||||||
|
// In this scenario, the new `nushell`'s PWD should be "#{pane_current_path}" rather init_cwd.
|
||||||
|
pub fn gather_parent_env_vars(engine_state: &mut EngineState, init_cwd: &Path) {
|
||||||
|
gather_env_vars(std::env::vars(), engine_state, init_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gather_env_vars(
|
||||||
|
vars: impl Iterator<Item = (String, String)>,
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
init_cwd: &Path,
|
||||||
|
) {
|
||||||
|
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
|
||||||
|
report_shell_error(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: format!("Environment variable was not captured: {env_str}"),
|
||||||
|
msg: "".into(),
|
||||||
|
span: None,
|
||||||
|
help: Some(msg.into()),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
|
||||||
|
fake_env_file.push_str(&escape_quote_string(name));
|
||||||
|
fake_env_file.push('=');
|
||||||
|
fake_env_file.push_str(&escape_quote_string(val));
|
||||||
|
fake_env_file.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fake_env_file = String::new();
|
||||||
|
// Write all the env vars into a fake file
|
||||||
|
for (name, val) in vars {
|
||||||
|
put_env_to_fake_file(&name, &val, &mut fake_env_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
match init_cwd.to_str() {
|
||||||
|
Some(cwd) => {
|
||||||
|
put_env_to_fake_file("PWD", cwd, &mut fake_env_file);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Could not capture current working directory
|
||||||
|
report_shell_error(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::GenericError {
|
||||||
|
error: "Current directory is not a valid utf-8 path".into(),
|
||||||
|
msg: "".into(),
|
||||||
|
span: None,
|
||||||
|
help: Some(format!(
|
||||||
|
"Retrieving current directory failed: {init_cwd:?} not a valid utf-8 path"
|
||||||
|
)),
|
||||||
|
inner: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lex the fake file, assign spans to all environment variables and add them
|
||||||
|
// to stack
|
||||||
|
let span_offset = engine_state.next_span_start();
|
||||||
|
|
||||||
|
engine_state.add_file(
|
||||||
|
"Host Environment Variables".into(),
|
||||||
|
fake_env_file.as_bytes().into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true);
|
||||||
|
|
||||||
|
for token in tokens {
|
||||||
|
if let Token {
|
||||||
|
contents: TokenContents::Item,
|
||||||
|
span: full_span,
|
||||||
|
} = token
|
||||||
|
{
|
||||||
|
let contents = engine_state.get_span_contents(full_span);
|
||||||
|
let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true);
|
||||||
|
|
||||||
|
let name = if let Some(Token {
|
||||||
|
contents: TokenContents::Item,
|
||||||
|
span,
|
||||||
|
}) = parts.first()
|
||||||
|
{
|
||||||
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
|
let bytes = working_set.get_span_contents(*span);
|
||||||
|
|
||||||
|
if bytes.len() < 2 {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got empty name.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (bytes, err) = unescape_unquote_string(bytes, *span);
|
||||||
|
if let Some(err) = err {
|
||||||
|
working_set.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !working_set.parse_errors.is_empty() {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got unparsable name.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes
|
||||||
|
} else {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got empty name.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = if let Some(Token {
|
||||||
|
contents: TokenContents::Item,
|
||||||
|
span,
|
||||||
|
}) = parts.get(2)
|
||||||
|
{
|
||||||
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
|
let bytes = working_set.get_span_contents(*span);
|
||||||
|
|
||||||
|
if bytes.len() < 2 {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got empty value.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (bytes, err) = unescape_unquote_string(bytes, *span);
|
||||||
|
if let Some(err) = err {
|
||||||
|
working_set.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !working_set.parse_errors.is_empty() {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got unparsable value.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::string(bytes, *span)
|
||||||
|
} else {
|
||||||
|
report_capture_error(
|
||||||
|
engine_state,
|
||||||
|
&String::from_utf8_lossy(contents),
|
||||||
|
"Got empty value.",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// stack.add_env_var(name, value);
|
||||||
|
engine_state.add_env_var(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a pipeline with formatting applied based on display_output hook.
|
||||||
|
///
|
||||||
|
/// This function should be preferred when printing values resulting from a completed evaluation.
|
||||||
|
/// For values printed as part of a command's execution, such as values printed by the `print` command,
|
||||||
|
/// the `PipelineData::print_table` function should be preferred instead as it is not config-dependent.
|
||||||
|
///
|
||||||
|
/// `no_newline` controls if we need to attach newline character to output.
|
||||||
|
pub fn print_pipeline(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
pipeline: PipelineData,
|
||||||
|
no_newline: bool,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
||||||
|
let pipeline = eval_hook(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
Some(pipeline),
|
||||||
|
vec![],
|
||||||
|
&hook,
|
||||||
|
"display_output",
|
||||||
|
)?;
|
||||||
|
pipeline.print_raw(engine_state, no_newline, false)
|
||||||
|
} else {
|
||||||
|
// if display_output isn't set, we should still prefer to print with some formatting
|
||||||
|
pipeline.print_table(engine_state, stack, no_newline, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_source(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
source: &[u8],
|
||||||
|
fname: &str,
|
||||||
|
input: PipelineData,
|
||||||
|
allow_return: bool,
|
||||||
|
) -> i32 {
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
|
||||||
|
Ok(failed) => {
|
||||||
|
let code = failed.into();
|
||||||
|
stack.set_last_exit_code(code, Span::unknown());
|
||||||
|
code
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
report_shell_error(engine_state, &err);
|
||||||
|
let code = err.exit_code();
|
||||||
|
stack.set_last_error(&err);
|
||||||
|
code.unwrap_or(0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = enable_vt_processing();
|
||||||
|
}
|
||||||
|
|
||||||
|
perf!(
|
||||||
|
&format!("eval_source {}", &fname),
|
||||||
|
start_time,
|
||||||
|
engine_state
|
||||||
|
.get_config()
|
||||||
|
.use_ansi_coloring
|
||||||
|
.get(engine_state)
|
||||||
|
);
|
||||||
|
|
||||||
|
exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_source(
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
source: &[u8],
|
||||||
|
fname: &str,
|
||||||
|
input: PipelineData,
|
||||||
|
allow_return: bool,
|
||||||
|
) -> Result<bool, 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_parse_warning(&working_set, warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = working_set.parse_errors.first() {
|
||||||
|
report_parse_error(&working_set, err);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = working_set.compile_errors.first() {
|
||||||
|
report_compile_error(&working_set, err);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
(output, working_set.render())
|
||||||
|
};
|
||||||
|
|
||||||
|
engine_state.merge_delta(delta)?;
|
||||||
|
|
||||||
|
let pipeline = if allow_return {
|
||||||
|
eval_block_with_early_return::<WithoutDebug>(engine_state, stack, &block, input)
|
||||||
|
} else {
|
||||||
|
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let no_newline = matches!(&pipeline, &PipelineData::ByteStream(..));
|
||||||
|
print_pipeline(engine_state, stack, pipeline, no_newline)?;
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gather_env_vars() {
|
||||||
|
let mut engine_state = EngineState::new();
|
||||||
|
let symbols = r##" !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"##;
|
||||||
|
|
||||||
|
gather_env_vars(
|
||||||
|
[
|
||||||
|
("FOO".into(), "foo".into()),
|
||||||
|
("SYMBOLS".into(), symbols.into()),
|
||||||
|
(symbols.into(), "symbols".into()),
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
&mut engine_state,
|
||||||
|
Path::new("t"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let env = engine_state.render_env_vars();
|
||||||
|
|
||||||
|
assert!(matches!(env.get("FOO"), Some(&Value::String { val, .. }) if val == "foo"));
|
||||||
|
assert!(matches!(env.get("SYMBOLS"), Some(&Value::String { val, .. }) if val == symbols));
|
||||||
|
assert!(matches!(env.get(symbols), Some(&Value::String { val, .. }) if val == "symbols"));
|
||||||
|
assert!(env.contains_key("PWD"));
|
||||||
|
assert_eq!(env.len(), 4);
|
||||||
|
}
|
||||||
|
}
|
27
nushell/crates/nu-cli/src/validation.rs
Normal file
27
nushell/crates/nu-cli/src/validation.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use nu_parser::parse;
|
||||||
|
use nu_protocol::{
|
||||||
|
ParseError,
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use reedline::{ValidationResult, Validator};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct NuValidator {
|
||||||
|
pub engine_state: Arc<EngineState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Validator for NuValidator {
|
||||||
|
fn validate(&self, line: &str) -> ValidationResult {
|
||||||
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
|
parse(&mut working_set, None, line.as_bytes(), false);
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
working_set.parse_errors.first(),
|
||||||
|
Some(ParseError::UnexpectedEof(..))
|
||||||
|
) {
|
||||||
|
ValidationResult::Incomplete
|
||||||
|
} else {
|
||||||
|
ValidationResult::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
296
nushell/crates/nu-cli/tests/commands/history_import.rs
Normal file
296
nushell/crates/nu-cli/tests/commands/history_import.rs
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
use nu_protocol::HistoryFileFormat;
|
||||||
|
use nu_test_support::{Outcome, nu};
|
||||||
|
use reedline::{
|
||||||
|
FileBackedHistory, History, HistoryItem, HistoryItemId, ReedlineError, SearchQuery,
|
||||||
|
SqliteBackedHistory,
|
||||||
|
};
|
||||||
|
use rstest::rstest;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct Test {
|
||||||
|
cfg_dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Test {
|
||||||
|
fn new(history_format: &'static str) -> Self {
|
||||||
|
let cfg_dir = tempfile::Builder::new()
|
||||||
|
.prefix("history_import_test")
|
||||||
|
.tempdir()
|
||||||
|
.unwrap();
|
||||||
|
// Assigning to $env.config.history.file_format seems to work only in startup
|
||||||
|
// configuration.
|
||||||
|
std::fs::write(
|
||||||
|
cfg_dir.path().join("env.nu"),
|
||||||
|
format!("$env.config.history.file_format = {history_format:?}"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
Self { cfg_dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nu(&self, cmd: impl AsRef<str>) -> Outcome {
|
||||||
|
let env = [(
|
||||||
|
"XDG_CONFIG_HOME".to_string(),
|
||||||
|
self.cfg_dir.path().to_str().unwrap().to_string(),
|
||||||
|
)];
|
||||||
|
let env_config = self.cfg_dir.path().join("env.nu");
|
||||||
|
nu!(envs: env, env_config: env_config, cmd.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_plaintext(&self) -> Result<FileBackedHistory, ReedlineError> {
|
||||||
|
FileBackedHistory::with_file(
|
||||||
|
100,
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Plaintext.default_file_name()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_sqlite(&self) -> Result<SqliteBackedHistory, ReedlineError> {
|
||||||
|
SqliteBackedHistory::with_file(
|
||||||
|
self.cfg_dir
|
||||||
|
.path()
|
||||||
|
.join("nushell")
|
||||||
|
.join(HistoryFileFormat::Sqlite.default_file_name()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_backend(&self, format: HistoryFileFormat) -> Result<Box<dyn History>, ReedlineError> {
|
||||||
|
fn boxed(be: impl History + 'static) -> Box<dyn History> {
|
||||||
|
Box::new(be)
|
||||||
|
}
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
match format {
|
||||||
|
Plaintext => self.open_plaintext().map(boxed),
|
||||||
|
Sqlite => self.open_sqlite().map(boxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HistorySource {
|
||||||
|
Vec(Vec<HistoryItem>),
|
||||||
|
Command(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestCase {
|
||||||
|
dst_format: HistoryFileFormat,
|
||||||
|
dst_history: Vec<HistoryItem>,
|
||||||
|
src_history: HistorySource,
|
||||||
|
want_history: Vec<HistoryItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_TEST_CASE: TestCase = TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
dst_history: Vec::new(),
|
||||||
|
src_history: HistorySource::Vec(Vec::new()),
|
||||||
|
want_history: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
impl TestCase {
|
||||||
|
fn run(self) {
|
||||||
|
use HistoryFileFormat::*;
|
||||||
|
let test = Test::new(match self.dst_format {
|
||||||
|
Plaintext => "plaintext",
|
||||||
|
Sqlite => "sqlite",
|
||||||
|
});
|
||||||
|
save_all(
|
||||||
|
&mut *test.open_backend(self.dst_format).unwrap(),
|
||||||
|
self.dst_history,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let outcome = match self.src_history {
|
||||||
|
HistorySource::Vec(src_history) => {
|
||||||
|
let src_format = match self.dst_format {
|
||||||
|
Plaintext => Sqlite,
|
||||||
|
Sqlite => Plaintext,
|
||||||
|
};
|
||||||
|
save_all(&mut *test.open_backend(src_format).unwrap(), src_history).unwrap();
|
||||||
|
test.nu("history import")
|
||||||
|
}
|
||||||
|
HistorySource::Command(cmd) => {
|
||||||
|
let mut cmd = cmd.to_string();
|
||||||
|
cmd.push_str(" | history import");
|
||||||
|
test.nu(cmd)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(outcome.status.success());
|
||||||
|
let got = query_all(&*test.open_backend(self.dst_format).unwrap()).unwrap();
|
||||||
|
|
||||||
|
// Compare just the commands first, for readability.
|
||||||
|
fn commands_only(items: &[HistoryItem]) -> Vec<&str> {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.command_line.as_str())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
assert_eq!(commands_only(&got), commands_only(&self.want_history));
|
||||||
|
// If commands match, compare full items.
|
||||||
|
assert_eq!(got, self.want_history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_all(history: &dyn History) -> Result<Vec<HistoryItem>, ReedlineError> {
|
||||||
|
history.search(SearchQuery::everything(
|
||||||
|
reedline::SearchDirection::Forward,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_all(history: &mut dyn History, items: Vec<HistoryItem>) -> Result<(), ReedlineError> {
|
||||||
|
for item in items {
|
||||||
|
history.save(item)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_ITEM: HistoryItem = HistoryItem {
|
||||||
|
command_line: String::new(),
|
||||||
|
id: None,
|
||||||
|
start_timestamp: None,
|
||||||
|
session_id: None,
|
||||||
|
hostname: None,
|
||||||
|
cwd: None,
|
||||||
|
duration: None,
|
||||||
|
exit_status: None,
|
||||||
|
more_info: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_string() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Command("echo bar"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_import_pipe_record() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Command("[[cwd command]; [/tmp some_command]]"),
|
||||||
|
want_history: vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "some_command".to_string(),
|
||||||
|
cwd: Some("/tmp".to_string()),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_plaintext() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Plaintext,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_empty_sqlite() {
|
||||||
|
TestCase {
|
||||||
|
dst_format: HistoryFileFormat::Sqlite,
|
||||||
|
src_history: HistorySource::Vec(vec![
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "foo".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "bar".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..EMPTY_TEST_CASE
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::plaintext(HistoryFileFormat::Plaintext)]
|
||||||
|
#[case::sqlite(HistoryFileFormat::Sqlite)]
|
||||||
|
fn to_existing(#[case] dst_format: HistoryFileFormat) {
|
||||||
|
TestCase {
|
||||||
|
dst_format,
|
||||||
|
dst_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
src_history: HistorySource::Vec(vec![HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
}]),
|
||||||
|
want_history: vec![
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(0)),
|
||||||
|
command_line: "original-1".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(1)),
|
||||||
|
command_line: "original-2".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
HistoryItem {
|
||||||
|
id: Some(HistoryItemId::new(2)),
|
||||||
|
command_line: "new".to_string(),
|
||||||
|
..EMPTY_ITEM
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
.run()
|
||||||
|
}
|
7
nushell/crates/nu-cli/tests/commands/keybindings_list.rs
Normal file
7
nushell/crates/nu-cli/tests/commands/keybindings_list.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use nu_test_support::nu;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_empty() {
|
||||||
|
let result = nu!("keybindings list | is-not-empty");
|
||||||
|
assert_eq!(result.out, "true");
|
||||||
|
}
|
3
nushell/crates/nu-cli/tests/commands/mod.rs
Normal file
3
nushell/crates/nu-cli/tests/commands/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod history_import;
|
||||||
|
mod keybindings_list;
|
||||||
|
mod nu_highlight;
|
7
nushell/crates/nu-cli/tests/commands/nu_highlight.rs
Normal file
7
nushell/crates/nu-cli/tests/commands/nu_highlight.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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");
|
||||||
|
}
|
2670
nushell/crates/nu-cli/tests/completions/mod.rs
Normal file
2670
nushell/crates/nu-cli/tests/completions/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,209 @@
|
|||||||
|
use nu_engine::eval_block;
|
||||||
|
use nu_parser::parse;
|
||||||
|
use nu_path::{AbsolutePathBuf, PathBuf};
|
||||||
|
use nu_protocol::{
|
||||||
|
PipelineData, ShellError, Span, Value,
|
||||||
|
debugger::WithoutDebug,
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
};
|
||||||
|
use nu_test_support::fs;
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use std::path::MAIN_SEPARATOR;
|
||||||
|
|
||||||
|
fn create_default_context() -> EngineState {
|
||||||
|
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_engine_helper(pwd: AbsolutePathBuf) -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||||
|
let pwd_str = pwd
|
||||||
|
.clone()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Create a new engine with default context
|
||||||
|
let mut engine_state = create_default_context();
|
||||||
|
|
||||||
|
// Add $nu
|
||||||
|
engine_state.generate_nu_constant();
|
||||||
|
|
||||||
|
// New stack
|
||||||
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
|
// Add pwd as env var
|
||||||
|
stack.add_env_var(
|
||||||
|
"PWD".to_string(),
|
||||||
|
Value::string(pwd_str.clone(), nu_protocol::Span::new(0, pwd_str.len())),
|
||||||
|
);
|
||||||
|
stack.add_env_var(
|
||||||
|
"TEST".to_string(),
|
||||||
|
Value::string(
|
||||||
|
"NUSHELL".to_string(),
|
||||||
|
nu_protocol::Span::new(0, pwd_str.len()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
#[cfg(windows)]
|
||||||
|
stack.add_env_var(
|
||||||
|
"Path".to_string(),
|
||||||
|
Value::string(
|
||||||
|
"c:\\some\\path;c:\\some\\other\\path".to_string(),
|
||||||
|
nu_protocol::Span::new(0, pwd_str.len()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
stack.add_env_var(
|
||||||
|
"PATH".to_string(),
|
||||||
|
Value::string(
|
||||||
|
"/some/path:/some/other/path".to_string(),
|
||||||
|
nu_protocol::Span::new(0, pwd_str.len()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge environment into the permanent state
|
||||||
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
|
(pwd, pwd_str, engine_state, stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// creates a new engine with the current path in the completions fixtures folder
|
||||||
|
pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||||
|
new_engine_helper(fs::fixtures().join("completions"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds pseudo PATH env for external completion tests
|
||||||
|
pub fn new_external_engine() -> EngineState {
|
||||||
|
let mut engine = create_default_context();
|
||||||
|
let dir = fs::fixtures().join("external_completions").join("path");
|
||||||
|
let dir_str = dir.to_string_lossy().to_string();
|
||||||
|
let internal_span = nu_protocol::Span::new(0, dir_str.len());
|
||||||
|
engine.add_env_var(
|
||||||
|
"PATH".to_string(),
|
||||||
|
Value::List {
|
||||||
|
vals: vec![Value::String {
|
||||||
|
val: dir_str,
|
||||||
|
internal_span,
|
||||||
|
}],
|
||||||
|
internal_span,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
engine
|
||||||
|
}
|
||||||
|
|
||||||
|
/// creates a new engine with the current path in the dotnu_completions fixtures folder
|
||||||
|
pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||||
|
// Target folder inside assets
|
||||||
|
let dir = fs::fixtures().join("dotnu_completions");
|
||||||
|
let (dir, dir_str, mut engine_state, mut stack) = new_engine_helper(dir);
|
||||||
|
let dir_span = nu_protocol::Span::new(0, dir_str.len());
|
||||||
|
|
||||||
|
// const $NU_LIB_DIRS
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
let var_id = working_set.add_variable(
|
||||||
|
b"$NU_LIB_DIRS".into(),
|
||||||
|
Span::unknown(),
|
||||||
|
nu_protocol::Type::List(Box::new(nu_protocol::Type::String)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
working_set.set_variable_const_val(
|
||||||
|
var_id,
|
||||||
|
Value::test_list(vec![
|
||||||
|
Value::string(file(dir.join("lib-dir1")), dir_span),
|
||||||
|
Value::string(file(dir.join("lib-dir3")), dir_span),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
let _ = engine_state.merge_delta(working_set.render());
|
||||||
|
|
||||||
|
stack.add_env_var(
|
||||||
|
"NU_LIB_DIRS".into(),
|
||||||
|
Value::test_list(vec![
|
||||||
|
Value::string(file(dir.join("lib-dir2")), dir_span),
|
||||||
|
Value::string(file(dir.join("lib-dir3")), dir_span),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge environment into the permanent state
|
||||||
|
let merge_result = engine_state.merge_env(&mut stack);
|
||||||
|
assert!(merge_result.is_ok());
|
||||||
|
|
||||||
|
(dir, dir_str, engine_state, stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||||
|
new_engine_helper(fs::fixtures().join("quoted_completions"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
||||||
|
new_engine_helper(fs::fixtures().join("partial_completions"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// match a list of suggestions with the expected values
|
||||||
|
pub fn match_suggestions(expected: &Vec<&str>, suggestions: &Vec<Suggestion>) {
|
||||||
|
let expected_len = expected.len();
|
||||||
|
let suggestions_len = suggestions.len();
|
||||||
|
if expected_len != suggestions_len {
|
||||||
|
panic!(
|
||||||
|
"\nexpected {expected_len} suggestions but got {suggestions_len}: \n\
|
||||||
|
Suggestions: {suggestions:#?} \n\
|
||||||
|
Expected: {expected:#?}\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let suggestions_str = suggestions
|
||||||
|
.iter()
|
||||||
|
.map(|it| it.value.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(expected, &suggestions_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// match a list of suggestions with the expected values
|
||||||
|
pub fn match_suggestions_by_string(expected: &[String], suggestions: &Vec<Suggestion>) {
|
||||||
|
let expected = expected.iter().map(|it| it.as_str()).collect::<Vec<_>>();
|
||||||
|
match_suggestions(&expected, suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// append the separator to the converted path
|
||||||
|
pub fn folder(path: impl Into<PathBuf>) -> String {
|
||||||
|
let mut converted_path = file(path);
|
||||||
|
converted_path.push(MAIN_SEPARATOR);
|
||||||
|
converted_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convert a given path to string
|
||||||
|
pub fn file(path: impl Into<PathBuf>) -> String {
|
||||||
|
path.into().into_os_string().into_string().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// merge_input executes the given input into the engine
|
||||||
|
/// and merges the state
|
||||||
|
pub fn merge_input(
|
||||||
|
input: &[u8],
|
||||||
|
engine_state: &mut EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
) -> Result<(), ShellError> {
|
||||||
|
let (block, delta) = {
|
||||||
|
let mut working_set = StateWorkingSet::new(engine_state);
|
||||||
|
|
||||||
|
let block = parse(&mut working_set, None, input, false);
|
||||||
|
|
||||||
|
assert!(working_set.parse_errors.is_empty());
|
||||||
|
|
||||||
|
(block, working_set.render())
|
||||||
|
};
|
||||||
|
|
||||||
|
engine_state.merge_delta(delta)?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
eval_block::<WithoutDebug>(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
&block,
|
||||||
|
PipelineData::Value(Value::nothing(Span::unknown()), None),
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge environment into the permanent state
|
||||||
|
engine_state.merge_env(stack)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user