forked from extern/nushell
Compare commits
139 Commits
Author | SHA1 | Date | |
---|---|---|---|
3783c19d02 | |||
1f6e0255c0 | |||
b61af9a26a | |||
6e2f6c71fe | |||
f51d5789a3 | |||
933cee27ae | |||
4566c904d0 | |||
9b020c056b | |||
60b5863058 | |||
836f914163 | |||
594006cfa0 | |||
57761149f4 | |||
521e28dcdc | |||
a30930324d | |||
625e807a35 | |||
d18f34daa4 | |||
4fd73ef54a | |||
58f395989a | |||
791e8a0e59 | |||
14066ccc30 | |||
683b912263 | |||
3bac480ca0 | |||
97eb8492a3 | |||
0892a16a3d | |||
0b85938415 | |||
aaec840b91 | |||
74d0f19291 | |||
7ce570e52c | |||
3a0eded0b8 | |||
5afd45414e | |||
6ed033737d | |||
d38a3a8b4e | |||
6b4cb8b0e0 | |||
48fa25fd42 | |||
bdfad6b1de | |||
4f974efeba | |||
e86c1b118e | |||
5e177fe8e7 | |||
4129f15eb9 | |||
690ec9abfa | |||
b2c52b51b7 | |||
888369022f | |||
4409185e1b | |||
ef1934a7ee | |||
591fb4bd36 | |||
12d3e4e424 | |||
c3bed1352a | |||
3ceb39c82c | |||
13869e7d52 | |||
121a4f06fb | |||
d0e636ae7a | |||
6e7e2dbb97 | |||
d64cf1687e | |||
657b631fdc | |||
fa6ed7a40b | |||
80f21d37e0 | |||
e2cf4cc7d6 | |||
abe028f930 | |||
ef1cf7e634 | |||
1e4b33b9c6 | |||
4654f03d31 | |||
608b6f3634 | |||
a86e6ce89b | |||
c4cfbaec2d | |||
d40109f210 | |||
20be8a4987 | |||
d6dd4078b1 | |||
6649da3f5d | |||
62901573d0 | |||
80c9888f82 | |||
19c3570cf9 | |||
2cb815b7b4 | |||
a088081695 | |||
4bb95a880f | |||
9beecff736 | |||
fa0400c3f2 | |||
1d2d31580b | |||
834f993547 | |||
2193910579 | |||
f692da487e | |||
0986c61a5d | |||
6a6471b04b | |||
05f7d7d38b | |||
d89ad4fafd | |||
385bc40627 | |||
e2d24c5956 | |||
82633e2df7 | |||
31a4fc41eb | |||
79182db587 | |||
a2872b4ccc | |||
0afa18ac4a | |||
1aef3a730a | |||
e934062542 | |||
2e3b74f1b2 | |||
a87f53072a | |||
047081fa72 | |||
5586d4a0a0 | |||
911fba8a8a | |||
2873e943b3 | |||
0c9dd6a29a | |||
a4410fef40 | |||
0011f4df56 | |||
ee5064abed | |||
82e3bb0f38 | |||
319930a1b9 | |||
a64e0956cd | |||
91e17d2f9f | |||
56a546e73d | |||
cf88c8eef3 | |||
3484e0defd | |||
79e4d35f01 | |||
71dd857926 | |||
7a789d68a2 | |||
8a9cc33aac | |||
66087b01e6 | |||
19fa41b114 | |||
91cd1717e9 | |||
12b85beecc | |||
2252833917 | |||
4e9c1067fb | |||
e505e57a7a | |||
d122827a30 | |||
b007290a4e | |||
7c92791eed | |||
80769b7197 | |||
9b5dff828d | |||
90013295aa | |||
ea7c8c237e | |||
5d5b02d8dc | |||
00b67d338d | |||
d32e878868 | |||
41af2e4b30 | |||
e9f9aab79f | |||
e826540037 | |||
a435a9924c | |||
02ed15b932 | |||
81e269c483 | |||
eceae26b0a | |||
ec5fd62f9f |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -5,7 +5,7 @@ body:
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: Thank you for your bug report. We are working diligently with our community to integrate our latest code base that we call [engine-q](https://github.com/nushell/engine-q). We would like your help with this by checking to see if this bug report is still needed in engine-q. Thank you for your patience while we ready the next version of nushell.
|
||||
description: Thank you for your bug report.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -5,7 +5,7 @@ body:
|
||||
id: problem
|
||||
attributes:
|
||||
label: Related problem
|
||||
description: Thank you for your feature request. We are working diligently with our community to integrate our latest code base that we call [engine-q](https://github.com/nushell/engine-q). We would like your help with this by checking to see if this feature request is still needed in engine-q. Thank you for your patience while we ready the next version of nushell.
|
||||
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 [...]
|
||||
|
1
.github/workflows/winget-submission.yml
vendored
1
.github/workflows/winget-submission.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: Submit Nushell package to Windows Package Manager Community Repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@
|
||||
history.txt
|
||||
tests/fixtures/nuplayground
|
||||
crates/*/target
|
||||
.mailmap
|
||||
|
||||
# Debian/Ubuntu
|
||||
debian/.debhelper/
|
||||
|
@ -71,5 +71,5 @@ cargo build
|
||||
- To view verbose logs when developing, enable the `trace` log level.
|
||||
|
||||
```shell
|
||||
cargo build --release --features=extra && cargo run --release --features=extra -- --loglevel trace
|
||||
cargo build --release --features=extra && cargo run --release --features=extra -- --log-level trace
|
||||
```
|
||||
|
447
Cargo.lock
generated
447
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
41
Cargo.toml
41
Cargo.toml
@ -10,7 +10,8 @@ license = "MIT"
|
||||
name = "nu"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/nushell/nushell"
|
||||
version = "0.60.0"
|
||||
rust-version = "1.59"
|
||||
version = "0.61.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@ -33,32 +34,30 @@ members = [
|
||||
[dependencies]
|
||||
chrono = "0.4.19"
|
||||
crossterm = "0.23.0"
|
||||
crossterm_winapi = "0.9.0"
|
||||
ctrlc = "3.2.1"
|
||||
log = "0.4"
|
||||
miette = "4.1.0"
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-cli = { path="./crates/nu-cli", version = "0.60.0" }
|
||||
nu-color-config = { path = "./crates/nu-color-config", version = "0.60.0" }
|
||||
nu-command = { path="./crates/nu-command", version = "0.60.0" }
|
||||
nu-engine = { path="./crates/nu-engine", version = "0.60.0" }
|
||||
nu-json = { path="./crates/nu-json", version = "0.60.0" }
|
||||
nu-parser = { path="./crates/nu-parser", version = "0.60.0" }
|
||||
nu-path = { path="./crates/nu-path", version = "0.60.0" }
|
||||
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.60.0" }
|
||||
nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.60.0" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.60.0" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.60.0" }
|
||||
nu-table = { path = "./crates/nu-table", version = "0.60.0" }
|
||||
nu-term-grid = { path = "./crates/nu-term-grid", version = "0.60.0" }
|
||||
nu-utils = { path = "./crates/nu-utils", version = "0.60.0" }
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-cli = { path="./crates/nu-cli", version = "0.61.0" }
|
||||
nu-color-config = { path = "./crates/nu-color-config", version = "0.61.0" }
|
||||
nu-command = { path="./crates/nu-command", version = "0.61.0" }
|
||||
nu-engine = { path="./crates/nu-engine", version = "0.61.0" }
|
||||
nu-json = { path="./crates/nu-json", version = "0.61.0" }
|
||||
nu-parser = { path="./crates/nu-parser", version = "0.61.0" }
|
||||
nu-path = { path="./crates/nu-path", version = "0.61.0" }
|
||||
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.61.0" }
|
||||
nu-pretty-hex = { path = "./crates/nu-pretty-hex", version = "0.61.0" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.61.0" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.61.0" }
|
||||
nu-table = { path = "./crates/nu-table", version = "0.61.0" }
|
||||
nu-term-grid = { path = "./crates/nu-term-grid", version = "0.61.0" }
|
||||
pretty_env_logger = "0.4.0"
|
||||
rayon = "1.5.1"
|
||||
reedline = "0.3.0"
|
||||
reedline = { version = "0.4.0", features = ["bashisms"]}
|
||||
is_executable = "1.0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path="./crates/nu-test-support" }
|
||||
nu-test-support = { path="./crates/nu-test-support", version = "0.61.0" }
|
||||
tempfile = "3.2.0"
|
||||
assert_cmd = "2.0.2"
|
||||
pretty_assertions = "1.0.0"
|
||||
@ -72,13 +71,13 @@ embed-resource = "1"
|
||||
|
||||
[features]
|
||||
plugin = ["nu-plugin", "nu-cli/plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin", "nu-engine/plugin"]
|
||||
default = ["plugin", "which", "zip-support", "trash-support"]
|
||||
default = ["plugin", "which-support", "zip-support", "trash-support"]
|
||||
stable = ["default"]
|
||||
extra = ["default", "dataframe"]
|
||||
wasi = []
|
||||
|
||||
# Stable (Default)
|
||||
which = ["nu-command/which"]
|
||||
which-support = ["nu-command/which-support"]
|
||||
zip-support = ["nu-command/zip"]
|
||||
trash-support = ["nu-command/trash-support"]
|
||||
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2021 The Nushell Project Developers
|
||||
Copyright (c) 2019 - 2022 The Nushell Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -43,7 +43,7 @@ You can also find information on more specific topics in our [cookbook](https://
|
||||
|
||||
Up-to-date installation instructions can be found in the [installation chapter of the book](https://www.nushell.sh/book/installation.html). **Windows users**: please note that Nu works on Windows 10 and does not currently have Windows 7/8.1 support.
|
||||
|
||||
To build Nu, you will need to use the **latest stable (1.51 or later)** version of the compiler.
|
||||
To build Nu, you will need to use the **latest stable (1.59 or later)** version of the compiler.
|
||||
|
||||
Required dependencies:
|
||||
|
||||
@ -273,6 +273,8 @@ Please submit an issue or PR to be added to this list.
|
||||
### Integrations
|
||||
- [zoxide](https://github.com/ajeetdsouza/zoxide)
|
||||
- [starship](https://github.com/starship/starship)
|
||||
- [oh-my-posh](https://ohmyposh.dev)
|
||||
- [Couchbase Shell](https://couchbase.sh)
|
||||
### Mentions
|
||||
- [The Python Launcher for Unix](https://github.com/brettcannon/python-launcher#how-do-i-get-a-table-of-python-executables-in-nushell)
|
||||
|
||||
|
@ -1,23 +1,24 @@
|
||||
[package]
|
||||
name = "nu-cli"
|
||||
version = "0.60.0"
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "CLI-related functionality for Nushell"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cli"
|
||||
version = "0.61.0"
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.60.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.60.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.60.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.60.0" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
|
||||
nu-color-config = { path = "../nu-color-config" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.61.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.61.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.61.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.61.0" }
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.61.0" }
|
||||
|
||||
crossterm = "0.23.0"
|
||||
crossterm_winapi = "0.9.0"
|
||||
miette = { version = "4.1.0", features = ["fancy"] }
|
||||
miette = { version = "4.4.0", features = ["fancy"] }
|
||||
thiserror = "1.0.29"
|
||||
reedline = "0.3.0"
|
||||
reedline = { version = "0.4.0", features = ["bashisms"]}
|
||||
|
||||
log = "0.4"
|
||||
is_executable = "1.0.1"
|
||||
|
21
crates/nu-cli/LICENSE
Normal file
21
crates/nu-cli/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2022 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.
|
@ -56,10 +56,6 @@ pub fn evaluate_commands(
|
||||
}
|
||||
};
|
||||
|
||||
// Make a note of the exceptions we see for externals that look like math expressions
|
||||
let exceptions = crate::util::external_exceptions(engine_state, stack);
|
||||
engine_state.external_exceptions = exceptions;
|
||||
|
||||
// Merge the delta in case env vars changed in the config
|
||||
match nu_engine::env::current_dir(engine_state, stack) {
|
||||
Ok(cwd) => {
|
||||
|
@ -1,636 +0,0 @@
|
||||
use nu_engine::eval_call;
|
||||
use nu_parser::{flatten_expression, parse, trim_quotes, FlatShape};
|
||||
use nu_protocol::{
|
||||
ast::{Call, Expr},
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Value, CONFIG_VARIABLE_ID,
|
||||
};
|
||||
use reedline::Completer;
|
||||
|
||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||
|
||||
pub struct CompletionOptions {
|
||||
case_sensitive: bool,
|
||||
positional: bool,
|
||||
sort: bool,
|
||||
}
|
||||
|
||||
impl Default for CompletionOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
case_sensitive: true,
|
||||
positional: true,
|
||||
sort: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NuCompleter {
|
||||
engine_state: EngineState,
|
||||
config: Option<Value>,
|
||||
}
|
||||
|
||||
impl NuCompleter {
|
||||
pub fn new(engine_state: EngineState, config: Option<Value>) -> Self {
|
||||
Self {
|
||||
engine_state,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
|
||||
let mut executables = vec![];
|
||||
|
||||
let paths = self.engine_state.env_vars.get("PATH");
|
||||
|
||||
if let Some(paths) = paths {
|
||||
if let Ok(paths) = paths.as_list() {
|
||||
for path in paths {
|
||||
let path = path.as_string().unwrap_or_default();
|
||||
|
||||
if let Ok(mut contents) = std::fs::read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if !executables.contains(
|
||||
&item
|
||||
.path()
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
) && matches!(
|
||||
item.path()
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().starts_with(prefix)),
|
||||
Some(true)
|
||||
) && is_executable::is_executable(&item.path())
|
||||
{
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executables
|
||||
}
|
||||
|
||||
fn complete_variables(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: &[u8],
|
||||
span: Span,
|
||||
offset: usize,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
let mut output = vec![];
|
||||
|
||||
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
|
||||
|
||||
for builtin in builtins {
|
||||
if builtin.as_bytes().starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
builtin.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for scope in &working_set.delta.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(v.0).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
for scope in &self.engine_state.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(v.0).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.dedup();
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn complete_commands(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
find_externals: bool,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
let prefix = working_set.get_span_contents(span);
|
||||
|
||||
let results = working_set
|
||||
.find_commands_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&x).to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
let results_aliases =
|
||||
working_set
|
||||
.find_aliases_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&x).to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
|
||||
|
||||
let prefix = working_set.get_span_contents(span);
|
||||
let prefix = String::from_utf8_lossy(prefix).to_string();
|
||||
if find_externals {
|
||||
let results_external =
|
||||
self.external_command_completion(&prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
x,
|
||||
)
|
||||
});
|
||||
|
||||
for external in results_external {
|
||||
if results.contains(&external) {
|
||||
results.push((external.0, format!("^{}", external.1)))
|
||||
} else {
|
||||
results.push(external)
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
let mut line = line.to_string();
|
||||
line.insert(pos, 'a');
|
||||
let pos = offset + pos;
|
||||
let (output, _err) = parse(
|
||||
&mut working_set,
|
||||
Some("completer"),
|
||||
line.as_bytes(),
|
||||
false,
|
||||
&[],
|
||||
);
|
||||
|
||||
for pipeline in output.pipelines.into_iter() {
|
||||
for expr in pipeline.expressions {
|
||||
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
||||
|
||||
for (flat_idx, flat) in flattened.iter().enumerate() {
|
||||
if pos >= flat.0.start && pos < flat.0.end {
|
||||
let new_span = Span {
|
||||
start: flat.0.start,
|
||||
end: flat.0.end - 1,
|
||||
};
|
||||
|
||||
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
|
||||
prefix.remove(pos - flat.0.start);
|
||||
|
||||
if prefix.starts_with(b"$") {
|
||||
let mut output =
|
||||
self.complete_variables(&working_set, &prefix, new_span, offset);
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
return output;
|
||||
}
|
||||
if prefix.starts_with(b"-") {
|
||||
// this might be a flag, let's see
|
||||
if let Expr::Call(call) = &expr.expr {
|
||||
let decl = working_set.get_decl(call.decl_id);
|
||||
let sig = decl.signature();
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
for named in &sig.named {
|
||||
if let Some(short) = named.short {
|
||||
let mut named = vec![0; short.len_utf8()];
|
||||
short.encode_utf8(&mut named);
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&named).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if named.long.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut named = named.long.as_bytes().to_vec();
|
||||
named.insert(0, b'-');
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&named).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
match &flat.1 {
|
||||
FlatShape::Custom(decl_id) => {
|
||||
//let prefix = working_set.get_span_contents(flat.0).to_vec();
|
||||
|
||||
let mut stack = Stack::new();
|
||||
// Set up our initial config to start from
|
||||
if let Some(conf) = &self.config {
|
||||
stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
|
||||
} else {
|
||||
stack.vars.insert(
|
||||
CONFIG_VARIABLE_ID,
|
||||
Value::Record {
|
||||
cols: vec![],
|
||||
vals: vec![],
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let result = eval_call(
|
||||
&self.engine_state,
|
||||
&mut stack,
|
||||
&Call {
|
||||
decl_id: *decl_id,
|
||||
head: new_span,
|
||||
positional: vec![],
|
||||
named: vec![],
|
||||
redirect_stdout: true,
|
||||
redirect_stderr: true,
|
||||
},
|
||||
PipelineData::new(new_span),
|
||||
);
|
||||
|
||||
fn map_completions<'a>(
|
||||
list: impl Iterator<Item = &'a Value>,
|
||||
new_span: Span,
|
||||
offset: usize,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
list.filter_map(move |x| {
|
||||
let s = x.as_string();
|
||||
|
||||
match s {
|
||||
Ok(s) => Some((
|
||||
reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
s,
|
||||
)),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let (completions, options) = match result {
|
||||
Ok(pd) => {
|
||||
let value = pd.into_value(new_span);
|
||||
match &value {
|
||||
Value::Record { .. } => {
|
||||
let completions = value
|
||||
.get_data_by_key("completions")
|
||||
.and_then(|val| {
|
||||
val.as_list().ok().map(|it| {
|
||||
map_completions(
|
||||
it.iter(),
|
||||
new_span,
|
||||
offset,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let options = value.get_data_by_key("options");
|
||||
|
||||
let options =
|
||||
if let Some(Value::Record { .. }) = &options {
|
||||
let options = options.unwrap_or_default();
|
||||
CompletionOptions {
|
||||
case_sensitive: options
|
||||
.get_data_by_key("case_sensitive")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
positional: options
|
||||
.get_data_by_key("positional")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
sort: options
|
||||
.get_data_by_key("sort")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
}
|
||||
} else {
|
||||
CompletionOptions::default()
|
||||
};
|
||||
|
||||
(completions, options)
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let completions =
|
||||
map_completions(vals.iter(), new_span, offset);
|
||||
(completions, CompletionOptions::default())
|
||||
}
|
||||
_ => (vec![], CompletionOptions::default()),
|
||||
}
|
||||
}
|
||||
_ => (vec![], CompletionOptions::default()),
|
||||
};
|
||||
|
||||
let mut completions: Vec<(reedline::Span, String)> = completions
|
||||
.into_iter()
|
||||
.filter(|it| {
|
||||
// Minimise clones for new functionality
|
||||
match (options.case_sensitive, options.positional) {
|
||||
(true, true) => it.1.as_bytes().starts_with(&prefix),
|
||||
(true, false) => it.1.contains(
|
||||
std::str::from_utf8(&prefix).unwrap_or(""),
|
||||
),
|
||||
(false, positional) => {
|
||||
let value = it.1.to_lowercase();
|
||||
let prefix = std::str::from_utf8(&prefix)
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
if positional {
|
||||
value.starts_with(&prefix)
|
||||
} else {
|
||||
value.contains(&prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if options.sort {
|
||||
completions.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
FlatShape::Filepath | FlatShape::GlobPattern => {
|
||||
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
|
||||
match d.as_string() {
|
||||
Ok(s) => s,
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||
let mut output: Vec<_> =
|
||||
file_path_completion(new_span, &prefix, &cwd)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
x.1,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
return output;
|
||||
}
|
||||
flat_shape => {
|
||||
let last = flattened
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|x| x.0.end > pos)
|
||||
.take_while(|x| {
|
||||
matches!(
|
||||
x.1,
|
||||
FlatShape::InternalCall
|
||||
| FlatShape::External
|
||||
| FlatShape::ExternalArg
|
||||
| FlatShape::Literal
|
||||
| FlatShape::String
|
||||
)
|
||||
})
|
||||
.last();
|
||||
|
||||
// The last item here would be the earliest shape that could possible by part of this subcommand
|
||||
let subcommands = if let Some(last) = last {
|
||||
self.complete_commands(
|
||||
&working_set,
|
||||
Span {
|
||||
start: last.0.start,
|
||||
end: pos,
|
||||
},
|
||||
offset,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if !subcommands.is_empty() {
|
||||
return subcommands;
|
||||
}
|
||||
|
||||
let commands =
|
||||
if matches!(flat_shape, nu_parser::FlatShape::External)
|
||||
|| matches!(flat_shape, nu_parser::FlatShape::InternalCall)
|
||||
|| ((new_span.end - new_span.start) == 0)
|
||||
{
|
||||
// we're in a gap or at a command
|
||||
self.complete_commands(&working_set, new_span, offset, true)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
|
||||
match d.as_string() {
|
||||
Ok(s) => s,
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let preceding_byte = if new_span.start > offset {
|
||||
working_set
|
||||
.get_span_contents(Span {
|
||||
start: new_span.start - 1,
|
||||
end: new_span.start,
|
||||
})
|
||||
.to_vec()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
// let prefix = working_set.get_span_contents(flat.0);
|
||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||
let mut output = file_path_completion(new_span, &prefix, &cwd)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
if flat_idx == 0 {
|
||||
// We're in the command position
|
||||
if x.1.starts_with('"')
|
||||
&& !matches!(preceding_byte.get(0), Some(b'^'))
|
||||
{
|
||||
let trimmed = trim_quotes(x.1.as_bytes());
|
||||
let trimmed =
|
||||
String::from_utf8_lossy(trimmed).to_string();
|
||||
let expanded =
|
||||
nu_path::canonicalize_with(trimmed, &cwd);
|
||||
|
||||
if let Ok(expanded) = expanded {
|
||||
if is_executable::is_executable(expanded) {
|
||||
(x.0, format!("^{}", x.1))
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
})
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
x.1,
|
||||
)
|
||||
})
|
||||
.chain(subcommands.into_iter())
|
||||
.chain(commands.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
//output.dedup_by(|a, b| a.1 == b.1);
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for NuCompleter {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
||||
self.completion_helper(line, pos)
|
||||
}
|
||||
}
|
||||
|
||||
fn file_path_completion(
|
||||
span: nu_protocol::Span,
|
||||
partial: &str,
|
||||
cwd: &str,
|
||||
) -> Vec<(nu_protocol::Span, String)> {
|
||||
use std::path::{is_separator, Path};
|
||||
|
||||
let partial = partial.replace('\'', "");
|
||||
|
||||
let (base_dir_name, partial) = {
|
||||
// If partial is only a word we want to search in the current dir
|
||||
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
|
||||
// On windows, this standardizes paths to use \
|
||||
let mut base = base.replace(is_separator, &SEP.to_string());
|
||||
|
||||
// rsplit_once removes the separator
|
||||
base.push(SEP);
|
||||
(base, rest)
|
||||
};
|
||||
|
||||
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
|
||||
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
|
||||
// which we don't want in this case (if we did, base_dir would already be ".")
|
||||
if base_dir == Path::new("") {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if let Ok(result) = base_dir.read_dir() {
|
||||
result
|
||||
.filter_map(|entry| {
|
||||
entry.ok().and_then(|entry| {
|
||||
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
if matches(partial, &file_name) {
|
||||
let mut path = format!("{}{}", base_dir_name, file_name);
|
||||
if entry.path().is_dir() {
|
||||
path.push(SEP);
|
||||
file_name.push(SEP);
|
||||
}
|
||||
|
||||
if path.contains(' ') {
|
||||
path = format!("\'{}\'", path);
|
||||
}
|
||||
|
||||
Some((span, path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(partial: &str, from: &str) -> bool {
|
||||
from.to_ascii_lowercase()
|
||||
.starts_with(&partial.to_ascii_lowercase())
|
||||
}
|
74
crates/nu-cli/src/completions/base.rs
Normal file
74
crates/nu-cli/src/completions/base.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use crate::completions::{CompletionOptions, SortBy};
|
||||
use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
|
||||
use reedline::Suggestion;
|
||||
|
||||
// Completer trait represents the three stages of the completion
|
||||
// fetch, filter and sort
|
||||
pub trait Completer {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions);
|
||||
|
||||
// Filter results using the completion options
|
||||
fn filter(
|
||||
&self,
|
||||
prefix: Vec<u8>,
|
||||
items: Vec<Suggestion>,
|
||||
options: CompletionOptions,
|
||||
) -> Vec<Suggestion> {
|
||||
items
|
||||
.into_iter()
|
||||
.filter(|it| {
|
||||
// Minimise clones for new functionality
|
||||
match (options.case_sensitive, options.positional) {
|
||||
(true, true) => it.value.as_bytes().starts_with(&prefix),
|
||||
(true, false) => it
|
||||
.value
|
||||
.contains(std::str::from_utf8(&prefix).unwrap_or("")),
|
||||
(false, positional) => {
|
||||
let value = it.value.to_lowercase();
|
||||
let prefix = std::str::from_utf8(&prefix).unwrap_or("").to_lowercase();
|
||||
if positional {
|
||||
value.starts_with(&prefix)
|
||||
} else {
|
||||
value.contains(&prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Sort results using the completion options
|
||||
fn sort(
|
||||
&self,
|
||||
items: Vec<Suggestion>,
|
||||
prefix: Vec<u8>,
|
||||
options: CompletionOptions,
|
||||
) -> Vec<Suggestion> {
|
||||
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
|
||||
let mut filtered_items = items;
|
||||
|
||||
// Sort items
|
||||
match options.sort_by {
|
||||
SortBy::LevenshteinDistance => {
|
||||
filtered_items.sort_by(|a, b| {
|
||||
let a_distance = levenshtein_distance(&prefix_str, &a.value);
|
||||
let b_distance = levenshtein_distance(&prefix_str, &b.value);
|
||||
a_distance.cmp(&b_distance)
|
||||
});
|
||||
}
|
||||
SortBy::Ascending => {
|
||||
filtered_items.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
}
|
||||
SortBy::None => {}
|
||||
};
|
||||
|
||||
filtered_items
|
||||
}
|
||||
}
|
274
crates/nu-cli/src/completions/command_completions.rs
Normal file
274
crates/nu-cli/src/completions/command_completions.rs
Normal file
@ -0,0 +1,274 @@
|
||||
use crate::completions::{
|
||||
file_completions::file_path_completion, Completer, CompletionOptions, SortBy,
|
||||
};
|
||||
use nu_parser::{trim_quotes, FlatShape};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CommandCompletion {
|
||||
engine_state: Arc<EngineState>,
|
||||
flattened: Vec<(Span, FlatShape)>,
|
||||
flat_idx: usize,
|
||||
flat_shape: FlatShape,
|
||||
}
|
||||
|
||||
impl CommandCompletion {
|
||||
pub fn new(
|
||||
engine_state: Arc<EngineState>,
|
||||
_: &StateWorkingSet,
|
||||
flattened: Vec<(Span, FlatShape)>,
|
||||
flat_idx: usize,
|
||||
flat_shape: FlatShape,
|
||||
) -> Self {
|
||||
Self {
|
||||
engine_state,
|
||||
flattened,
|
||||
flat_idx,
|
||||
flat_shape,
|
||||
}
|
||||
}
|
||||
|
||||
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
|
||||
let mut executables = vec![];
|
||||
|
||||
let paths = self.engine_state.env_vars.get("PATH");
|
||||
|
||||
if let Some(paths) = paths {
|
||||
if let Ok(paths) = paths.as_list() {
|
||||
for path in paths {
|
||||
let path = path.as_string().unwrap_or_default();
|
||||
|
||||
if let Ok(mut contents) = std::fs::read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if !executables.contains(
|
||||
&item
|
||||
.path()
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
.unwrap_or_default(),
|
||||
) && matches!(
|
||||
item.path()
|
||||
.file_name()
|
||||
.map(|x| x.to_string_lossy().starts_with(prefix)),
|
||||
Some(true)
|
||||
) && is_executable::is_executable(&item.path())
|
||||
{
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executables
|
||||
}
|
||||
|
||||
fn complete_commands(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
find_externals: bool,
|
||||
) -> Vec<Suggestion> {
|
||||
let prefix = working_set.get_span_contents(span);
|
||||
|
||||
let results = working_set
|
||||
.find_commands_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| Suggestion {
|
||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
||||
description: x.1,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
let results_aliases =
|
||||
working_set
|
||||
.find_aliases_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| Suggestion {
|
||||
value: String::from_utf8_lossy(&x).to_string(),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
|
||||
|
||||
let prefix = working_set.get_span_contents(span);
|
||||
let prefix = String::from_utf8_lossy(prefix).to_string();
|
||||
let results = if find_externals {
|
||||
let results_external =
|
||||
self.external_command_completion(&prefix)
|
||||
.into_iter()
|
||||
.map(move |x| Suggestion {
|
||||
value: x,
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
for external in results_external {
|
||||
if results.contains(&external) {
|
||||
results.push(Suggestion {
|
||||
value: format!("^{}", external.value),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: external.span,
|
||||
})
|
||||
} else {
|
||||
results.push(external)
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
results
|
||||
};
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for CommandCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||
let last = self
|
||||
.flattened
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|x| x.0.end > pos)
|
||||
.take_while(|x| {
|
||||
matches!(
|
||||
x.1,
|
||||
FlatShape::InternalCall
|
||||
| FlatShape::External
|
||||
| FlatShape::ExternalArg
|
||||
| FlatShape::Literal
|
||||
| FlatShape::String
|
||||
)
|
||||
})
|
||||
.last();
|
||||
|
||||
// Options
|
||||
let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance);
|
||||
|
||||
// The last item here would be the earliest shape that could possible by part of this subcommand
|
||||
let subcommands = if let Some(last) = last {
|
||||
self.complete_commands(
|
||||
working_set,
|
||||
Span {
|
||||
start: last.0.start,
|
||||
end: pos,
|
||||
},
|
||||
offset,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if !subcommands.is_empty() {
|
||||
return (subcommands, options);
|
||||
}
|
||||
|
||||
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
||||
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall)
|
||||
|| ((span.end - span.start) == 0)
|
||||
{
|
||||
// we're in a gap or at a command
|
||||
self.complete_commands(working_set, span, offset, true)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
|
||||
match d.as_string() {
|
||||
Ok(s) => s,
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let preceding_byte = if span.start > offset {
|
||||
working_set
|
||||
.get_span_contents(Span {
|
||||
start: span.start - 1,
|
||||
end: span.start,
|
||||
})
|
||||
.to_vec()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
// let prefix = working_set.get_span_contents(flat.0);
|
||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||
let output = file_path_completion(span, &prefix, &cwd)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
if self.flat_idx == 0 {
|
||||
// We're in the command position
|
||||
if x.1.starts_with('"') && !matches!(preceding_byte.get(0), Some(b'^')) {
|
||||
let trimmed = trim_quotes(x.1.as_bytes());
|
||||
let trimmed = String::from_utf8_lossy(trimmed).to_string();
|
||||
let expanded = nu_path::canonicalize_with(trimmed, &cwd);
|
||||
|
||||
if let Ok(expanded) = expanded {
|
||||
if is_executable::is_executable(expanded) {
|
||||
(x.0, format!("^{}", x.1))
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
} else {
|
||||
(x.0, x.1)
|
||||
}
|
||||
})
|
||||
.map(move |x| Suggestion {
|
||||
value: x.1,
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
})
|
||||
.chain(subcommands.into_iter())
|
||||
.chain(commands.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(output, options)
|
||||
}
|
||||
|
||||
// Replace base filter with no filter once all the results are already based in the current path
|
||||
fn filter(&self, _: Vec<u8>, items: Vec<Suggestion>, _: CompletionOptions) -> Vec<Suggestion> {
|
||||
items
|
||||
}
|
||||
}
|
174
crates/nu-cli/src/completions/completer.rs
Normal file
174
crates/nu-cli/src/completions/completer.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use crate::completions::{
|
||||
CommandCompletion, Completer, CustomCompletion, FileCompletion, FlagCompletion,
|
||||
VariableCompletion,
|
||||
};
|
||||
use nu_parser::{flatten_expression, parse, FlatShape};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
Span, Value,
|
||||
};
|
||||
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NuCompleter {
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: Stack,
|
||||
config: Option<Value>,
|
||||
}
|
||||
|
||||
impl NuCompleter {
|
||||
pub fn new(engine_state: Arc<EngineState>, stack: Stack, config: Option<Value>) -> Self {
|
||||
Self {
|
||||
engine_state,
|
||||
stack,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
// Process the completion for a given completer
|
||||
fn process_completion<T: Completer>(
|
||||
&self,
|
||||
completer: &mut T,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
new_span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
) -> Vec<Suggestion> {
|
||||
// Fetch
|
||||
let (mut suggestions, options) =
|
||||
completer.fetch(working_set, prefix.clone(), new_span, offset, pos);
|
||||
|
||||
// Filter
|
||||
suggestions = completer.filter(prefix.clone(), suggestions, options.clone());
|
||||
|
||||
// Sort
|
||||
suggestions = completer.sort(suggestions, prefix, options);
|
||||
|
||||
suggestions
|
||||
}
|
||||
|
||||
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
let mut line = line.to_string();
|
||||
line.insert(pos, 'a');
|
||||
let pos = offset + pos;
|
||||
let (output, _err) = parse(
|
||||
&mut working_set,
|
||||
Some("completer"),
|
||||
line.as_bytes(),
|
||||
false,
|
||||
&[],
|
||||
);
|
||||
|
||||
for pipeline in output.pipelines.into_iter() {
|
||||
for expr in pipeline.expressions {
|
||||
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
||||
|
||||
for (flat_idx, flat) in flattened.iter().enumerate() {
|
||||
if pos >= flat.0.start && pos < flat.0.end {
|
||||
// Create a new span
|
||||
let new_span = Span {
|
||||
start: flat.0.start,
|
||||
end: flat.0.end - 1,
|
||||
};
|
||||
|
||||
// Parses the prefix
|
||||
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
|
||||
prefix.remove(pos - flat.0.start);
|
||||
|
||||
// Variables completion
|
||||
if prefix.starts_with(b"$") {
|
||||
let mut completer = VariableCompletion::new(self.engine_state.clone());
|
||||
|
||||
return self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
|
||||
// Flags completion
|
||||
if prefix.starts_with(b"-") {
|
||||
let mut completer = FlagCompletion::new(expr);
|
||||
|
||||
return self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
|
||||
// Match other types
|
||||
match &flat.1 {
|
||||
FlatShape::Custom(decl_id) => {
|
||||
let mut completer = CustomCompletion::new(
|
||||
self.engine_state.clone(),
|
||||
self.stack.clone(),
|
||||
self.config.clone(),
|
||||
*decl_id,
|
||||
line,
|
||||
);
|
||||
|
||||
return self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
FlatShape::Filepath | FlatShape::GlobPattern => {
|
||||
let mut completer = FileCompletion::new(self.engine_state.clone());
|
||||
|
||||
return self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
flat_shape => {
|
||||
let mut completer = CommandCompletion::new(
|
||||
self.engine_state.clone(),
|
||||
&working_set,
|
||||
flattened.clone(),
|
||||
flat_idx,
|
||||
flat_shape.clone(),
|
||||
);
|
||||
|
||||
return self.process_completion(
|
||||
&mut completer,
|
||||
&working_set,
|
||||
prefix,
|
||||
new_span,
|
||||
offset,
|
||||
pos,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vec![];
|
||||
}
|
||||
}
|
||||
|
||||
impl ReedlineCompleter for NuCompleter {
|
||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
self.completion_helper(line, pos)
|
||||
}
|
||||
}
|
33
crates/nu-cli/src/completions/completion_options.rs
Normal file
33
crates/nu-cli/src/completions/completion_options.rs
Normal file
@ -0,0 +1,33 @@
|
||||
#[derive(Clone)]
|
||||
pub enum SortBy {
|
||||
LevenshteinDistance,
|
||||
Ascending,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionOptions {
|
||||
pub case_sensitive: bool,
|
||||
pub positional: bool,
|
||||
pub sort_by: SortBy,
|
||||
}
|
||||
|
||||
impl CompletionOptions {
|
||||
pub fn new(case_sensitive: bool, positional: bool, sort_by: SortBy) -> Self {
|
||||
Self {
|
||||
case_sensitive,
|
||||
positional,
|
||||
sort_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CompletionOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
case_sensitive: true,
|
||||
positional: true,
|
||||
sort_by: SortBy::Ascending,
|
||||
}
|
||||
}
|
||||
}
|
171
crates/nu-cli/src/completions/custom_completions.rs
Normal file
171
crates/nu-cli/src/completions/custom_completions.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use crate::completions::{Completer, CompletionOptions, SortBy};
|
||||
use nu_engine::eval_call;
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Call, Expr, Expression},
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CustomCompletion {
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: Stack,
|
||||
config: Option<Value>,
|
||||
decl_id: usize,
|
||||
line: String,
|
||||
}
|
||||
|
||||
impl CustomCompletion {
|
||||
pub fn new(
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: Stack,
|
||||
config: Option<Value>,
|
||||
decl_id: usize,
|
||||
line: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
engine_state,
|
||||
stack,
|
||||
config,
|
||||
decl_id,
|
||||
line,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_completions<'a>(
|
||||
&self,
|
||||
list: impl Iterator<Item = &'a Value>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
) -> Vec<Suggestion> {
|
||||
list.filter_map(move |x| {
|
||||
let s = x.as_string();
|
||||
|
||||
match s {
|
||||
Ok(s) => Some(Suggestion {
|
||||
value: s,
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
}),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for CustomCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
_: &StateWorkingSet,
|
||||
_: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||
// Line position
|
||||
let line_pos = pos - offset;
|
||||
|
||||
// Set up our initial config to start from
|
||||
if let Some(conf) = &self.config {
|
||||
self.stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
|
||||
} else {
|
||||
self.stack.vars.insert(
|
||||
CONFIG_VARIABLE_ID,
|
||||
Value::Record {
|
||||
cols: vec![],
|
||||
vals: vec![],
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Call custom declaration
|
||||
let result = eval_call(
|
||||
&self.engine_state,
|
||||
&mut self.stack,
|
||||
&Call {
|
||||
decl_id: self.decl_id,
|
||||
head: span,
|
||||
arguments: vec![
|
||||
Argument::Positional(Expression {
|
||||
span: Span { start: 0, end: 0 },
|
||||
ty: Type::String,
|
||||
expr: Expr::String(self.line.clone()),
|
||||
custom_completion: None,
|
||||
}),
|
||||
Argument::Positional(Expression {
|
||||
span: Span { start: 0, end: 0 },
|
||||
ty: Type::Int,
|
||||
expr: Expr::Int(line_pos as i64),
|
||||
custom_completion: None,
|
||||
}),
|
||||
],
|
||||
redirect_stdout: true,
|
||||
redirect_stderr: true,
|
||||
},
|
||||
PipelineData::new(span),
|
||||
);
|
||||
|
||||
// Parse result
|
||||
let (suggestions, options) = match result {
|
||||
Ok(pd) => {
|
||||
let value = pd.into_value(span);
|
||||
match &value {
|
||||
Value::Record { .. } => {
|
||||
let completions = value
|
||||
.get_data_by_key("completions")
|
||||
.and_then(|val| {
|
||||
val.as_list()
|
||||
.ok()
|
||||
.map(|it| self.map_completions(it.iter(), span, offset))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let options = value.get_data_by_key("options");
|
||||
|
||||
let options = if let Some(Value::Record { .. }) = &options {
|
||||
let options = options.unwrap_or_default();
|
||||
let should_sort = options
|
||||
.get_data_by_key("sort")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
CompletionOptions {
|
||||
case_sensitive: options
|
||||
.get_data_by_key("case_sensitive")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
positional: options
|
||||
.get_data_by_key("positional")
|
||||
.and_then(|val| val.as_bool().ok())
|
||||
.unwrap_or(true),
|
||||
sort_by: if should_sort {
|
||||
SortBy::Ascending
|
||||
} else {
|
||||
SortBy::None
|
||||
},
|
||||
}
|
||||
} else {
|
||||
CompletionOptions::default()
|
||||
};
|
||||
|
||||
(completions, options)
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let completions = self.map_completions(vals.iter(), span, offset);
|
||||
(completions, CompletionOptions::default())
|
||||
}
|
||||
_ => (vec![], CompletionOptions::default()),
|
||||
}
|
||||
}
|
||||
_ => (vec![], CompletionOptions::default()),
|
||||
};
|
||||
|
||||
(suggestions, options)
|
||||
}
|
||||
}
|
163
crates/nu-cli/src/completions/file_completions.rs
Normal file
163
crates/nu-cli/src/completions/file_completions.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use crate::completions::{Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
levenshtein_distance, Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::path::{is_separator, Path};
|
||||
use std::sync::Arc;
|
||||
|
||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileCompletion {
|
||||
engine_state: Arc<EngineState>,
|
||||
}
|
||||
|
||||
impl FileCompletion {
|
||||
pub fn new(engine_state: Arc<EngineState>) -> Self {
|
||||
Self { engine_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for FileCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
_: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") {
|
||||
match d.as_string() {
|
||||
Ok(s) => s,
|
||||
Err(_) => "".to_string(),
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||
let output: Vec<_> = file_path_completion(span, &prefix, &cwd)
|
||||
.into_iter()
|
||||
.map(move |x| Suggestion {
|
||||
value: x.1,
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Options
|
||||
let options = CompletionOptions::default();
|
||||
|
||||
(output, options)
|
||||
}
|
||||
|
||||
// Sort results prioritizing the non hidden folders
|
||||
fn sort(
|
||||
&self,
|
||||
items: Vec<Suggestion>,
|
||||
prefix: Vec<u8>,
|
||||
_: CompletionOptions, // Ignore the given options, once it's a custom sorting
|
||||
) -> Vec<Suggestion> {
|
||||
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
|
||||
|
||||
// Sort items
|
||||
let mut sorted_items = items;
|
||||
sorted_items.sort_by(|a, b| {
|
||||
let a_distance = levenshtein_distance(&prefix_str, &a.value);
|
||||
let b_distance = levenshtein_distance(&prefix_str, &b.value);
|
||||
a_distance.cmp(&b_distance)
|
||||
});
|
||||
|
||||
// Separate the results between hidden and non hidden
|
||||
let mut hidden: Vec<Suggestion> = vec![];
|
||||
let mut non_hidden: Vec<Suggestion> = vec![];
|
||||
|
||||
for item in sorted_items.into_iter() {
|
||||
let item_path = Path::new(&item.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
|
||||
}
|
||||
|
||||
// Replace base filter with no filter once all the results are already based in the current path
|
||||
fn filter(&self, _: Vec<u8>, items: Vec<Suggestion>, _: CompletionOptions) -> Vec<Suggestion> {
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_path_completion(
|
||||
span: nu_protocol::Span,
|
||||
partial: &str,
|
||||
cwd: &str,
|
||||
) -> Vec<(nu_protocol::Span, String)> {
|
||||
let partial = partial.replace('\'', "");
|
||||
|
||||
let (base_dir_name, partial) = {
|
||||
// If partial is only a word we want to search in the current dir
|
||||
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", &partial));
|
||||
// On windows, this standardizes paths to use \
|
||||
let mut base = base.replace(is_separator, &SEP.to_string());
|
||||
|
||||
// rsplit_once removes the separator
|
||||
base.push(SEP);
|
||||
(base, rest)
|
||||
};
|
||||
|
||||
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
|
||||
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
|
||||
// which we don't want in this case (if we did, base_dir would already be ".")
|
||||
if base_dir == Path::new("") {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if let Ok(result) = base_dir.read_dir() {
|
||||
return result
|
||||
.filter_map(|entry| {
|
||||
entry.ok().and_then(|entry| {
|
||||
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
if matches(partial, &file_name) {
|
||||
let mut path = format!("{}{}", base_dir_name, file_name);
|
||||
if entry.path().is_dir() {
|
||||
path.push(SEP);
|
||||
file_name.push(SEP);
|
||||
}
|
||||
|
||||
if path.contains(' ') {
|
||||
path = format!("\'{}\'", path);
|
||||
}
|
||||
|
||||
Some((span, path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn matches(partial: &str, from: &str) -> bool {
|
||||
from.to_ascii_lowercase()
|
||||
.starts_with(&partial.to_ascii_lowercase())
|
||||
}
|
81
crates/nu-cli/src/completions/flag_completions.rs
Normal file
81
crates/nu-cli/src/completions/flag_completions.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use crate::completions::{Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
ast::{Expr, Expression},
|
||||
engine::StateWorkingSet,
|
||||
Span,
|
||||
};
|
||||
|
||||
use reedline::Suggestion;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlagCompletion {
|
||||
expression: Expression,
|
||||
}
|
||||
|
||||
impl FlagCompletion {
|
||||
pub fn new(expression: Expression) -> Self {
|
||||
Self { expression }
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for FlagCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||
// Check if it's a flag
|
||||
if let Expr::Call(call) = &self.expression.expr {
|
||||
let decl = working_set.get_decl(call.decl_id);
|
||||
let sig = decl.signature();
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
for named in &sig.named {
|
||||
let flag_desc = &named.desc;
|
||||
if let Some(short) = named.short {
|
||||
let mut named = vec![0; short.len_utf8()];
|
||||
short.encode_utf8(&mut named);
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: Some(flag_desc.to_string()),
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if named.long.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut named = named.long.as_bytes().to_vec();
|
||||
named.insert(0, b'-');
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: Some(flag_desc.to_string()),
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (output, CompletionOptions::default());
|
||||
}
|
||||
|
||||
(vec![], CompletionOptions::default())
|
||||
}
|
||||
}
|
17
crates/nu-cli/src/completions/mod.rs
Normal file
17
crates/nu-cli/src/completions/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
mod base;
|
||||
mod command_completions;
|
||||
mod completer;
|
||||
mod completion_options;
|
||||
mod custom_completions;
|
||||
mod file_completions;
|
||||
mod flag_completions;
|
||||
mod variable_completions;
|
||||
|
||||
pub use base::Completer;
|
||||
pub use command_completions::CommandCompletion;
|
||||
pub use completer::NuCompleter;
|
||||
pub use completion_options::{CompletionOptions, SortBy};
|
||||
pub use custom_completions::CustomCompletion;
|
||||
pub use file_completions::{file_path_completion, FileCompletion};
|
||||
pub use flag_completions::FlagCompletion;
|
||||
pub use variable_completions::VariableCompletion;
|
82
crates/nu-cli/src/completions/variable_completions.rs
Normal file
82
crates/nu-cli/src/completions/variable_completions.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use crate::completions::{Completer, CompletionOptions};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
Span,
|
||||
};
|
||||
use reedline::Suggestion;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VariableCompletion {
|
||||
engine_state: Arc<EngineState>,
|
||||
}
|
||||
|
||||
impl VariableCompletion {
|
||||
pub fn new(engine_state: Arc<EngineState>) -> Self {
|
||||
Self { engine_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for VariableCompletion {
|
||||
fn fetch(
|
||||
&mut self,
|
||||
working_set: &StateWorkingSet,
|
||||
prefix: Vec<u8>,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
_: usize,
|
||||
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||
let mut output = vec![];
|
||||
|
||||
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
|
||||
|
||||
for builtin in builtins {
|
||||
if builtin.as_bytes().starts_with(&prefix) {
|
||||
output.push(Suggestion {
|
||||
value: builtin.to_string(),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for scope in &working_set.delta.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(&prefix) {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for scope in &self.engine_state.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(&prefix) {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.dedup();
|
||||
|
||||
(output, CompletionOptions::default())
|
||||
}
|
||||
}
|
@ -27,10 +27,6 @@ pub fn evaluate_file(
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Make a note of the exceptions we see for externals that look like math expressions
|
||||
let exceptions = crate::util::external_exceptions(engine_state, stack);
|
||||
engine_state.external_exceptions = exceptions;
|
||||
|
||||
let file = std::fs::read(&path).into_diagnostic()?;
|
||||
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
|
@ -3,6 +3,7 @@ mod completions;
|
||||
mod config_files;
|
||||
mod errors;
|
||||
mod eval_file;
|
||||
mod menus;
|
||||
mod nu_highlight;
|
||||
mod print;
|
||||
mod prompt;
|
||||
@ -18,6 +19,7 @@ pub use completions::NuCompleter;
|
||||
pub use config_files::eval_config_contents;
|
||||
pub use errors::CliError;
|
||||
pub use eval_file::evaluate_file;
|
||||
pub use menus::{DescriptionMenu, NuHelpCompleter};
|
||||
pub use nu_highlight::NuHighlight;
|
||||
pub use print::Print;
|
||||
pub use prompt::NushellPrompt;
|
||||
|
724
crates/nu-cli/src/menus/description_menu.rs
Normal file
724
crates/nu-cli/src/menus/description_menu.rs
Normal file
@ -0,0 +1,724 @@
|
||||
use {
|
||||
nu_ansi_term::{ansi::RESET, Style},
|
||||
reedline::{
|
||||
menu_functions::string_difference, Completer, LineBuffer, Menu, MenuEvent, MenuTextStyle,
|
||||
Painter, Suggestion,
|
||||
},
|
||||
};
|
||||
|
||||
/// Default values used as reference for the menu. These values are set during
|
||||
/// the initial declaration of the menu and are always kept as reference for the
|
||||
/// changeable [`WorkingDetails`]
|
||||
struct DefaultMenuDetails {
|
||||
/// Number of columns that the menu will have
|
||||
pub columns: u16,
|
||||
/// Column width
|
||||
pub col_width: Option<usize>,
|
||||
/// Column padding
|
||||
pub col_padding: usize,
|
||||
/// Number of rows for commands
|
||||
pub selection_rows: u16,
|
||||
/// Number of rows allowed to display the description
|
||||
pub description_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for DefaultMenuDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: 4,
|
||||
col_width: None,
|
||||
col_padding: 2,
|
||||
selection_rows: 4,
|
||||
description_rows: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the actual column conditions of the menu. These conditions change
|
||||
/// since they need to accommodate possible different line sizes for the column values
|
||||
#[derive(Default)]
|
||||
struct WorkingDetails {
|
||||
/// Number of columns that the menu will have
|
||||
pub columns: u16,
|
||||
/// Column width
|
||||
pub col_width: usize,
|
||||
/// Number of rows for description
|
||||
pub description_rows: usize,
|
||||
}
|
||||
|
||||
/// Completion menu definition
|
||||
pub struct DescriptionMenu {
|
||||
/// Menu name
|
||||
name: String,
|
||||
/// Menu status
|
||||
active: bool,
|
||||
/// Menu coloring
|
||||
color: MenuTextStyle,
|
||||
/// Default column details that are set when creating the menu
|
||||
/// These values are the reference for the working details
|
||||
default_details: DefaultMenuDetails,
|
||||
/// Number of minimum rows that are displayed when
|
||||
/// the required lines is larger than the available lines
|
||||
min_rows: u16,
|
||||
/// Working column details keep changing based on the collected values
|
||||
working_details: WorkingDetails,
|
||||
/// Menu cached values
|
||||
values: Vec<Suggestion>,
|
||||
/// column position of the cursor. Starts from 0
|
||||
col_pos: u16,
|
||||
/// row position in the menu. Starts from 0
|
||||
row_pos: u16,
|
||||
/// Menu marker when active
|
||||
marker: String,
|
||||
/// Event sent to the menu
|
||||
event: Option<MenuEvent>,
|
||||
/// String collected after the menu is activated
|
||||
input: Option<String>,
|
||||
/// Examples to select
|
||||
examples: Vec<String>,
|
||||
/// Example index
|
||||
example_index: Option<usize>,
|
||||
/// Examples may not be shown if there is not enough space in the screen
|
||||
show_examples: bool,
|
||||
/// Skipped description rows
|
||||
skipped_rows: usize,
|
||||
/// Calls the completer using only the line buffer difference difference
|
||||
/// after the menu was activated
|
||||
only_buffer_difference: bool,
|
||||
}
|
||||
|
||||
impl Default for DescriptionMenu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "description_menu".to_string(),
|
||||
active: false,
|
||||
color: MenuTextStyle::default(),
|
||||
default_details: DefaultMenuDetails::default(),
|
||||
min_rows: 3,
|
||||
working_details: WorkingDetails::default(),
|
||||
values: Vec::new(),
|
||||
col_pos: 0,
|
||||
row_pos: 0,
|
||||
marker: "? ".to_string(),
|
||||
event: None,
|
||||
input: None,
|
||||
examples: Vec::new(),
|
||||
example_index: None,
|
||||
show_examples: true,
|
||||
skipped_rows: 0,
|
||||
only_buffer_difference: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Menu configuration
|
||||
impl DescriptionMenu {
|
||||
/// Menu builder with new name
|
||||
pub fn with_name(mut self, name: &str) -> Self {
|
||||
self.name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_text_style(mut self, text_style: Style) -> Self {
|
||||
self.color.text_style = text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self {
|
||||
self.color.selected_text_style = selected_text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
|
||||
self.color.description_style = description_text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new columns value
|
||||
pub fn with_columns(mut self, columns: u16) -> Self {
|
||||
self.default_details.columns = columns;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new column width value
|
||||
pub fn with_column_width(mut self, col_width: Option<usize>) -> Self {
|
||||
self.default_details.col_width = col_width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new column width value
|
||||
pub fn with_column_padding(mut self, col_padding: usize) -> Self {
|
||||
self.default_details.col_padding = col_padding;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new selection rows value
|
||||
pub fn with_selection_rows(mut self, selection_rows: u16) -> Self {
|
||||
self.default_details.selection_rows = selection_rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new description rows value
|
||||
pub fn with_description_rows(mut self, description_rows: usize) -> Self {
|
||||
self.default_details.description_rows = description_rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with marker
|
||||
pub fn with_marker(mut self, marker: String) -> Self {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new only buffer difference
|
||||
pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
|
||||
self.only_buffer_difference = only_buffer_difference;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Menu functionality
|
||||
impl DescriptionMenu {
|
||||
/// Move menu cursor to the next element
|
||||
fn move_next(&mut self) {
|
||||
let mut new_col = self.col_pos + 1;
|
||||
let mut new_row = self.row_pos;
|
||||
|
||||
if new_col >= self.get_cols() {
|
||||
new_row += 1;
|
||||
new_col = 0;
|
||||
}
|
||||
|
||||
if new_row >= self.get_rows() {
|
||||
new_row = 0;
|
||||
new_col = 0;
|
||||
}
|
||||
|
||||
let position = new_row * self.get_cols() + new_col;
|
||||
if position >= self.get_values().len() as u16 {
|
||||
self.reset_position();
|
||||
} else {
|
||||
self.col_pos = new_col;
|
||||
self.row_pos = new_row;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move menu cursor to the previous element
|
||||
fn move_previous(&mut self) {
|
||||
let new_col = self.col_pos.checked_sub(1);
|
||||
|
||||
let (new_col, new_row) = match new_col {
|
||||
Some(col) => (col, self.row_pos),
|
||||
None => match self.row_pos.checked_sub(1) {
|
||||
Some(row) => (self.get_cols().saturating_sub(1), row),
|
||||
None => (
|
||||
self.get_cols().saturating_sub(1),
|
||||
self.get_rows().saturating_sub(1),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let position = new_row * self.get_cols() + new_col;
|
||||
if position >= self.get_values().len() as u16 {
|
||||
self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1);
|
||||
self.row_pos = self.get_rows().saturating_sub(1);
|
||||
} else {
|
||||
self.col_pos = new_col;
|
||||
self.row_pos = new_row;
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu index based on column and row position
|
||||
fn index(&self) -> usize {
|
||||
let index = self.row_pos * self.get_cols() + self.col_pos;
|
||||
index as usize
|
||||
}
|
||||
|
||||
/// Get selected value from the menu
|
||||
fn get_value(&self) -> Option<Suggestion> {
|
||||
self.get_values().get(self.index()).cloned()
|
||||
}
|
||||
|
||||
/// Calculates how many rows the Menu will use
|
||||
fn get_rows(&self) -> u16 {
|
||||
let values = self.get_values().len() as u16;
|
||||
|
||||
if values == 0 {
|
||||
// When the values are empty the no_records_msg is shown, taking 1 line
|
||||
return 1;
|
||||
}
|
||||
|
||||
let rows = values / self.get_cols();
|
||||
if values % self.get_cols() != 0 {
|
||||
rows + 1
|
||||
} else {
|
||||
rows
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns working details col width
|
||||
fn get_width(&self) -> usize {
|
||||
self.working_details.col_width
|
||||
}
|
||||
|
||||
/// Reset menu position
|
||||
fn reset_position(&mut self) {
|
||||
self.col_pos = 0;
|
||||
self.row_pos = 0;
|
||||
self.skipped_rows = 0;
|
||||
}
|
||||
|
||||
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
|
||||
let msg = "TYPE TO START SEACH";
|
||||
if use_ansi_coloring {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.color.selected_text_style.prefix(),
|
||||
msg,
|
||||
RESET
|
||||
)
|
||||
} else {
|
||||
msg.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns working details columns
|
||||
fn get_cols(&self) -> u16 {
|
||||
self.working_details.columns.max(1)
|
||||
}
|
||||
|
||||
/// End of line for menu
|
||||
fn end_of_line(&self, column: u16, index: usize) -> &str {
|
||||
let is_last = index == self.values.len().saturating_sub(1);
|
||||
if column == self.get_cols().saturating_sub(1) || is_last {
|
||||
"\r\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Update list of examples from the actual value
|
||||
fn update_examples(&mut self) {
|
||||
self.examples = self
|
||||
.get_value()
|
||||
.and_then(|suggestion| suggestion.extra)
|
||||
.unwrap_or_default();
|
||||
|
||||
self.example_index = None;
|
||||
}
|
||||
|
||||
/// Creates default string that represents one suggestion from the menu
|
||||
fn create_entry_string(
|
||||
&self,
|
||||
suggestion: &Suggestion,
|
||||
index: usize,
|
||||
column: u16,
|
||||
empty_space: usize,
|
||||
use_ansi_coloring: bool,
|
||||
) -> String {
|
||||
if use_ansi_coloring {
|
||||
if index == self.index() {
|
||||
format!(
|
||||
"{}{}{}{:>empty$}{}",
|
||||
self.color.selected_text_style.prefix(),
|
||||
&suggestion.value,
|
||||
RESET,
|
||||
"",
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}{:>empty$}{}",
|
||||
self.color.text_style.prefix(),
|
||||
&suggestion.value,
|
||||
RESET,
|
||||
"",
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If no ansi coloring is found, then the selection word is
|
||||
// the line in uppercase
|
||||
let (marker, empty_space) = if index == self.index() {
|
||||
(">", empty_space.saturating_sub(1))
|
||||
} else {
|
||||
("", empty_space)
|
||||
};
|
||||
|
||||
let line = format!(
|
||||
"{}{}{:>empty$}{}",
|
||||
marker,
|
||||
&suggestion.value,
|
||||
"",
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
);
|
||||
|
||||
if index == self.index() {
|
||||
line.to_uppercase()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Description string with color
|
||||
fn create_description_string(&self, use_ansi_coloring: bool) -> String {
|
||||
let description = self
|
||||
.get_value()
|
||||
.and_then(|suggestion| suggestion.description)
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.skip(self.skipped_rows)
|
||||
.take(self.working_details.description_rows)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\r\n");
|
||||
|
||||
if use_ansi_coloring && !description.is_empty() {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.color.description_style.prefix(),
|
||||
description,
|
||||
RESET,
|
||||
)
|
||||
} else {
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
/// Selectable list of examples from the actual value
|
||||
fn create_example_string(&self, use_ansi_coloring: bool) -> String {
|
||||
if !self.show_examples {
|
||||
return "".into();
|
||||
}
|
||||
|
||||
let examples: String = self
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, example)| {
|
||||
if let Some(example_index) = self.example_index {
|
||||
if index == example_index {
|
||||
format!(
|
||||
" {}{}{}\r\n",
|
||||
self.color.selected_text_style.prefix(),
|
||||
example,
|
||||
RESET
|
||||
)
|
||||
} else {
|
||||
format!(" {}\r\n", example)
|
||||
}
|
||||
} else {
|
||||
format!(" {}\r\n", example)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if examples.is_empty() {
|
||||
"".into()
|
||||
} else if use_ansi_coloring {
|
||||
format!(
|
||||
"{}\r\n\r\nExamples:\r\n{}{}",
|
||||
self.color.description_style.prefix(),
|
||||
RESET,
|
||||
examples,
|
||||
)
|
||||
} else {
|
||||
format!("\r\n\r\nExamples:\r\n{}", examples,)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Menu for DescriptionMenu {
|
||||
/// Menu name
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Menu indicator
|
||||
fn indicator(&self) -> &str {
|
||||
self.marker.as_str()
|
||||
}
|
||||
|
||||
/// Deactivates context menu
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// The menu stays active even with one record
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The menu does not need to partially complete
|
||||
fn can_partially_complete(
|
||||
&mut self,
|
||||
_values_updated: bool,
|
||||
_line_buffer: &mut LineBuffer,
|
||||
_completer: &mut dyn Completer,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Selects what type of event happened with the menu
|
||||
fn menu_event(&mut self, event: MenuEvent) {
|
||||
match &event {
|
||||
MenuEvent::Activate(_) => self.active = true,
|
||||
MenuEvent::Deactivate => {
|
||||
self.active = false;
|
||||
self.input = None;
|
||||
self.values = Vec::new();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
self.event = Some(event);
|
||||
}
|
||||
|
||||
/// Updates menu values
|
||||
fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &mut dyn Completer) {
|
||||
if self.only_buffer_difference {
|
||||
if let Some(old_string) = &self.input {
|
||||
let (start, input) = string_difference(line_buffer.get_buffer(), old_string);
|
||||
if !input.is_empty() {
|
||||
self.reset_position();
|
||||
self.values = completer.complete(input, start);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let trimmed_buffer = line_buffer.get_buffer().replace('\n', " ");
|
||||
self.values =
|
||||
completer.complete(trimmed_buffer.as_str(), line_buffer.insertion_point());
|
||||
self.reset_position();
|
||||
}
|
||||
}
|
||||
|
||||
/// The working details for the menu changes based on the size of the lines
|
||||
/// collected from the completer
|
||||
fn update_working_details(
|
||||
&mut self,
|
||||
line_buffer: &mut LineBuffer,
|
||||
completer: &mut dyn Completer,
|
||||
painter: &Painter,
|
||||
) {
|
||||
if let Some(event) = self.event.take() {
|
||||
// Updating all working parameters from the menu before executing any of the
|
||||
// possible event
|
||||
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
|
||||
let str_len = suggestion.value.len() + self.default_details.col_padding;
|
||||
if str_len > acc {
|
||||
str_len
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
});
|
||||
|
||||
// If no default width is found, then the total screen width is used to estimate
|
||||
// the column width based on the default number of columns
|
||||
let default_width = if let Some(col_width) = self.default_details.col_width {
|
||||
col_width
|
||||
} else {
|
||||
let col_width = painter.screen_width() / self.default_details.columns;
|
||||
col_width as usize
|
||||
};
|
||||
|
||||
// Adjusting the working width of the column based the max line width found
|
||||
// in the menu values
|
||||
if max_width > default_width {
|
||||
self.working_details.col_width = max_width;
|
||||
} else {
|
||||
self.working_details.col_width = default_width;
|
||||
};
|
||||
|
||||
// The working columns is adjusted based on possible number of columns
|
||||
// that could be fitted in the screen with the calculated column width
|
||||
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
|
||||
if possible_cols > self.default_details.columns {
|
||||
self.working_details.columns = self.default_details.columns.max(1);
|
||||
} else {
|
||||
self.working_details.columns = possible_cols;
|
||||
}
|
||||
|
||||
// Updating the working rows to display the description
|
||||
if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() {
|
||||
self.working_details.description_rows = self.default_details.description_rows;
|
||||
self.show_examples = true;
|
||||
} else {
|
||||
self.working_details.description_rows = painter
|
||||
.remaining_lines()
|
||||
.saturating_sub(self.default_details.selection_rows + 1)
|
||||
as usize;
|
||||
|
||||
self.show_examples = false;
|
||||
}
|
||||
|
||||
match event {
|
||||
MenuEvent::Activate(_) => {
|
||||
self.reset_position();
|
||||
self.input = Some(line_buffer.get_buffer().to_string());
|
||||
self.update_values(line_buffer, completer);
|
||||
}
|
||||
MenuEvent::Deactivate => self.active = false,
|
||||
MenuEvent::Edit(_) => {
|
||||
self.reset_position();
|
||||
self.update_values(line_buffer, completer);
|
||||
self.update_examples()
|
||||
}
|
||||
MenuEvent::NextElement => {
|
||||
self.skipped_rows = 0;
|
||||
self.move_next();
|
||||
self.update_examples();
|
||||
}
|
||||
MenuEvent::PreviousElement => {
|
||||
self.skipped_rows = 0;
|
||||
self.move_previous();
|
||||
self.update_examples();
|
||||
}
|
||||
MenuEvent::MoveUp => {
|
||||
if let Some(example_index) = self.example_index {
|
||||
if let Some(index) = example_index.checked_sub(1) {
|
||||
self.example_index = Some(index);
|
||||
} else {
|
||||
self.example_index = Some(self.examples.len().saturating_sub(1));
|
||||
}
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
}
|
||||
MenuEvent::MoveDown => {
|
||||
if let Some(example_index) = self.example_index {
|
||||
let index = example_index + 1;
|
||||
if index < self.examples.len() {
|
||||
self.example_index = Some(index);
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
}
|
||||
MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1),
|
||||
MenuEvent::MoveRight => {
|
||||
let skipped = self.skipped_rows + 1;
|
||||
let description_rows = self
|
||||
.get_value()
|
||||
.and_then(|suggestion| suggestion.description)
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.count();
|
||||
|
||||
let allowed_skips =
|
||||
description_rows.saturating_sub(self.working_details.description_rows);
|
||||
|
||||
if skipped < allowed_skips {
|
||||
self.skipped_rows = skipped;
|
||||
} else {
|
||||
self.skipped_rows = allowed_skips;
|
||||
}
|
||||
}
|
||||
MenuEvent::PreviousPage | MenuEvent::NextPage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The buffer gets replaced in the Span location
|
||||
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
|
||||
if let Some(Suggestion { value, span, .. }) = self.get_value() {
|
||||
let start = span.start.min(line_buffer.len());
|
||||
let end = span.end.min(line_buffer.len());
|
||||
|
||||
let string_len = if let Some(example_index) = self.example_index {
|
||||
let example = self
|
||||
.examples
|
||||
.get(example_index)
|
||||
.expect("the example index is always checked");
|
||||
|
||||
line_buffer.replace(start..end, example);
|
||||
example.len()
|
||||
} else {
|
||||
line_buffer.replace(start..end, &value);
|
||||
value.len()
|
||||
};
|
||||
|
||||
let mut offset = line_buffer.insertion_point();
|
||||
offset += string_len.saturating_sub(end.saturating_sub(start));
|
||||
line_buffer.set_insertion_point(offset);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum rows that should be displayed by the menu
|
||||
fn min_rows(&self) -> u16 {
|
||||
self.get_rows().min(self.min_rows)
|
||||
}
|
||||
|
||||
/// Gets values from filler that will be displayed in the menu
|
||||
fn get_values(&self) -> &[Suggestion] {
|
||||
&self.values
|
||||
}
|
||||
|
||||
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
|
||||
let example_lines = self
|
||||
.examples
|
||||
.iter()
|
||||
.fold(0, |acc, example| example.lines().count() + acc);
|
||||
|
||||
self.default_details.selection_rows
|
||||
+ self.default_details.description_rows as u16
|
||||
+ example_lines as u16
|
||||
+ 3
|
||||
}
|
||||
|
||||
fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String {
|
||||
if self.get_values().is_empty() {
|
||||
self.no_records_msg(use_ansi_coloring)
|
||||
} else {
|
||||
// The skip values represent the number of lines that should be skipped
|
||||
// while printing the menu
|
||||
let available_lines = self.default_details.selection_rows;
|
||||
let skip_values = if self.row_pos >= available_lines {
|
||||
let skip_lines = self.row_pos.saturating_sub(available_lines) + 1;
|
||||
(skip_lines * self.get_cols()) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// It seems that crossterm prefers to have a complete string ready to be printed
|
||||
// rather than looping through the values and printing multiple things
|
||||
// This reduces the flickering when printing the menu
|
||||
let available_values = (available_lines * self.get_cols()) as usize;
|
||||
let selection_values: String = self
|
||||
.get_values()
|
||||
.iter()
|
||||
.skip(skip_values)
|
||||
.take(available_values)
|
||||
.enumerate()
|
||||
.map(|(index, suggestion)| {
|
||||
// Correcting the enumerate index based on the number of skipped values
|
||||
let index = index + skip_values;
|
||||
let column = index as u16 % self.get_cols();
|
||||
let empty_space = self.get_width().saturating_sub(suggestion.value.len());
|
||||
|
||||
self.create_entry_string(
|
||||
suggestion,
|
||||
index,
|
||||
column,
|
||||
empty_space,
|
||||
use_ansi_coloring,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
format!(
|
||||
"{}{}{}",
|
||||
selection_values,
|
||||
self.create_description_string(use_ansi_coloring),
|
||||
self.create_example_string(use_ansi_coloring)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
105
crates/nu-cli/src/menus/help_completions.rs
Normal file
105
crates/nu-cli/src/menus/help_completions.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_protocol::{engine::EngineState, levenshtein_distance};
|
||||
use reedline::{Completer, Suggestion};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct NuHelpCompleter(Arc<EngineState>);
|
||||
|
||||
impl NuHelpCompleter {
|
||||
pub fn new(engine_state: Arc<EngineState>) -> Self {
|
||||
Self(engine_state)
|
||||
}
|
||||
|
||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let full_commands = self.0.get_signatures_with_examples(false);
|
||||
|
||||
//Vec<(Signature, Vec<Example>, bool, bool)> {
|
||||
let mut commands = full_commands
|
||||
.iter()
|
||||
.filter(|(sig, _, _, _)| {
|
||||
sig.name.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig.usage.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig
|
||||
.extra_usage
|
||||
.to_lowercase()
|
||||
.contains(&line.to_lowercase())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
commands.sort_by(|(a, _, _, _), (b, _, _, _)| {
|
||||
let a_distance = levenshtein_distance(line, &a.name);
|
||||
let b_distance = levenshtein_distance(line, &b.name);
|
||||
a_distance.cmp(&b_distance)
|
||||
});
|
||||
|
||||
commands
|
||||
.into_iter()
|
||||
.map(|(sig, examples, _, _)| {
|
||||
let mut long_desc = String::new();
|
||||
|
||||
let usage = &sig.usage;
|
||||
if !usage.is_empty() {
|
||||
long_desc.push_str(usage);
|
||||
long_desc.push_str("\r\n\r\n");
|
||||
}
|
||||
|
||||
let extra_usage = &sig.extra_usage;
|
||||
if !extra_usage.is_empty() {
|
||||
long_desc.push_str(extra_usage);
|
||||
long_desc.push_str("\r\n\r\n");
|
||||
}
|
||||
|
||||
long_desc.push_str(&format!("Usage:\r\n > {}\r\n", sig.call_signature()));
|
||||
|
||||
if !sig.named.is_empty() {
|
||||
long_desc.push_str(&get_flags_section(sig))
|
||||
}
|
||||
|
||||
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 {
|
||||
long_desc
|
||||
.push_str(&format!(" {}: {}\r\n", positional.name, positional.desc));
|
||||
}
|
||||
for positional in &sig.optional_positional {
|
||||
long_desc.push_str(&format!(
|
||||
" (optional) {}: {}\r\n",
|
||||
positional.name, positional.desc
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(rest_positional) = &sig.rest_positional {
|
||||
long_desc.push_str(&format!(
|
||||
" ...{}: {}\r\n",
|
||||
rest_positional.name, rest_positional.desc
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let extra: Vec<String> = examples
|
||||
.iter()
|
||||
.map(|example| example.example.to_string())
|
||||
.collect();
|
||||
|
||||
Suggestion {
|
||||
value: sig.name.clone(),
|
||||
description: Some(long_desc),
|
||||
extra: Some(extra),
|
||||
span: reedline::Span {
|
||||
start: pos,
|
||||
end: pos + line.len(),
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for NuHelpCompleter {
|
||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
self.completion_helper(line, pos)
|
||||
}
|
||||
}
|
174
crates/nu-cli/src/menus/menu_completions.rs
Normal file
174
crates/nu-cli/src/menus/menu_completions.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use nu_engine::eval_block;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
IntoPipelineData, Span, Value,
|
||||
};
|
||||
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
|
||||
use std::sync::Arc;
|
||||
|
||||
const SELECTION_CHAR: char = '!';
|
||||
|
||||
pub struct NuMenuCompleter {
|
||||
block_id: usize,
|
||||
span: Span,
|
||||
stack: Stack,
|
||||
engine_state: Arc<EngineState>,
|
||||
only_buffer_difference: bool,
|
||||
}
|
||||
|
||||
impl NuMenuCompleter {
|
||||
pub fn new(
|
||||
block_id: usize,
|
||||
span: Span,
|
||||
stack: Stack,
|
||||
engine_state: Arc<EngineState>,
|
||||
only_buffer_difference: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
block_id,
|
||||
span,
|
||||
stack,
|
||||
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 {
|
||||
val: parsed.remainder.to_string(),
|
||||
span: 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 {
|
||||
val: pos as i64,
|
||||
span: self.span,
|
||||
};
|
||||
self.stack.add_var(*position_id, line_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
let input = Value::nothing(self.span).into_pipeline_data();
|
||||
let res = eval_block(
|
||||
&self.engine_state,
|
||||
&mut self.stack,
|
||||
block,
|
||||
input,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
if let Ok(values) = res {
|
||||
let values = values.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 { .. } => {
|
||||
let text = match value
|
||||
.get_data_by_key("value")
|
||||
.and_then(|val| val.as_string().ok())
|
||||
{
|
||||
Some(val) => val,
|
||||
None => "No value key".to_string(),
|
||||
};
|
||||
|
||||
let description = value
|
||||
.get_data_by_key("description")
|
||||
.and_then(|val| val.as_string().ok());
|
||||
|
||||
let span = match value.get_data_by_key("span") {
|
||||
Some(span @ Value::Record { .. }) => {
|
||||
let start = span
|
||||
.get_data_by_key("start")
|
||||
.and_then(|val| val.as_integer().ok());
|
||||
let end = span
|
||||
.get_data_by_key("end")
|
||||
.and_then(|val| val.as_integer().ok());
|
||||
match (start, end) {
|
||||
(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 } else { 0 },
|
||||
end: if only_buffer_difference {
|
||||
pos + line.len()
|
||||
} else {
|
||||
line.len()
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => reedline::Span {
|
||||
start: if only_buffer_difference { pos } else { 0 },
|
||||
end: if only_buffer_difference {
|
||||
pos + line.len()
|
||||
} else {
|
||||
line.len()
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let extra = match value.get_data_by_key("extra") {
|
||||
Some(Value::List { vals, .. }) => {
|
||||
let extra: Vec<String> = vals
|
||||
.into_iter()
|
||||
.filter_map(|extra| match extra {
|
||||
Value::String { val, .. } => Some(val),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(extra)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
vec![Suggestion {
|
||||
value: text,
|
||||
description,
|
||||
extra,
|
||||
span,
|
||||
}]
|
||||
}
|
||||
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),
|
||||
description: None,
|
||||
extra: None,
|
||||
span: reedline::Span {
|
||||
start: 0,
|
||||
end: line.len(),
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
7
crates/nu-cli/src/menus/mod.rs
Normal file
7
crates/nu-cli/src/menus/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod description_menu;
|
||||
mod help_completions;
|
||||
mod menu_completions;
|
||||
|
||||
pub use description_menu::DescriptionMenu;
|
||||
pub use help_completions::NuHelpCompleter;
|
||||
pub use menu_completions::NuMenuCompleter;
|
@ -12,10 +12,10 @@ use {
|
||||
pub struct NushellPrompt {
|
||||
left_prompt_string: Option<String>,
|
||||
right_prompt_string: Option<String>,
|
||||
default_prompt_indicator: String,
|
||||
default_vi_insert_prompt_indicator: String,
|
||||
default_vi_normal_prompt_indicator: String,
|
||||
default_multiline_indicator: 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>,
|
||||
}
|
||||
|
||||
impl Default for NushellPrompt {
|
||||
@ -29,10 +29,10 @@ impl NushellPrompt {
|
||||
NushellPrompt {
|
||||
left_prompt_string: None,
|
||||
right_prompt_string: None,
|
||||
default_prompt_indicator: "〉".to_string(),
|
||||
default_vi_insert_prompt_indicator: ": ".to_string(),
|
||||
default_vi_normal_prompt_indicator: "〉".to_string(),
|
||||
default_multiline_indicator: "::: ".to_string(),
|
||||
default_prompt_indicator: None,
|
||||
default_vi_insert_prompt_indicator: None,
|
||||
default_vi_normal_prompt_indicator: None,
|
||||
default_multiline_indicator: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,19 +44,19 @@ impl NushellPrompt {
|
||||
self.right_prompt_string = prompt_string;
|
||||
}
|
||||
|
||||
pub fn update_prompt_indicator(&mut self, prompt_indicator_string: String) {
|
||||
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: 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: 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: String) {
|
||||
pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: Option<String>) {
|
||||
self.default_multiline_indicator = prompt_multiline_indicator_string;
|
||||
}
|
||||
|
||||
@ -64,18 +64,19 @@ impl NushellPrompt {
|
||||
&mut self,
|
||||
left_prompt_string: Option<String>,
|
||||
right_prompt_string: Option<String>,
|
||||
prompt_indicator_string: String,
|
||||
prompt_multiline_indicator_string: String,
|
||||
prompt_vi: (String, String),
|
||||
prompt_indicator_string: Option<String>,
|
||||
prompt_multiline_indicator_string: Option<String>,
|
||||
prompt_vi: (Option<String>, Option<String>),
|
||||
) {
|
||||
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.default_multiline_indicator = prompt_multiline_indicator_string;
|
||||
}
|
||||
|
||||
fn default_wrapped_custom_string(&self, str: String) -> String {
|
||||
@ -112,18 +113,33 @@ impl Prompt for NushellPrompt {
|
||||
|
||||
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
|
||||
match edit_mode {
|
||||
PromptEditMode::Default => self.default_prompt_indicator.as_str().into(),
|
||||
PromptEditMode::Emacs => self.default_prompt_indicator.as_str().into(),
|
||||
PromptEditMode::Default => match &self.default_prompt_indicator {
|
||||
Some(indicator) => indicator.as_str().into(),
|
||||
None => "〉".into(),
|
||||
},
|
||||
PromptEditMode::Emacs => match &self.default_prompt_indicator {
|
||||
Some(indicator) => indicator.as_str().into(),
|
||||
None => "〉".into(),
|
||||
},
|
||||
PromptEditMode::Vi(vi_mode) => match vi_mode {
|
||||
PromptViMode::Normal => self.default_vi_normal_prompt_indicator.as_str().into(),
|
||||
PromptViMode::Insert => self.default_vi_insert_prompt_indicator.as_str().into(),
|
||||
PromptViMode::Normal => match &self.default_vi_normal_prompt_indicator {
|
||||
Some(indicator) => indicator.as_str().into(),
|
||||
None => ": ".into(),
|
||||
},
|
||||
PromptViMode::Insert => match &self.default_vi_insert_prompt_indicator {
|
||||
Some(indicator) => indicator.as_str().into(),
|
||||
None => "〉".into(),
|
||||
},
|
||||
},
|
||||
PromptEditMode::Custom(str) => self.default_wrapped_custom_string(str).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
|
||||
Cow::Borrowed(self.default_multiline_indicator.as_str())
|
||||
match &self.default_multiline_indicator {
|
||||
Some(indicator) => indicator.as_str().into(),
|
||||
None => "::: ".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_history_search_indicator(
|
||||
|
@ -2,7 +2,6 @@ use crate::util::report_error;
|
||||
use crate::NushellPrompt;
|
||||
use log::info;
|
||||
use nu_engine::eval_subexpression;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
Config, PipelineData, Span, Value,
|
||||
@ -17,49 +16,6 @@ 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) fn get_prompt_indicators(
|
||||
config: &Config,
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
is_perf_true: bool,
|
||||
) -> (String, String, String, String) {
|
||||
let prompt_indicator = match stack.get_env_var(engine_state, PROMPT_INDICATOR) {
|
||||
Some(pi) => pi.into_string("", config),
|
||||
None => "〉".to_string(),
|
||||
};
|
||||
|
||||
let prompt_vi_insert = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_INSERT) {
|
||||
Some(pvii) => pvii.into_string("", config),
|
||||
None => ": ".to_string(),
|
||||
};
|
||||
|
||||
let prompt_vi_normal = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_NORMAL) {
|
||||
Some(pviv) => pviv.into_string("", config),
|
||||
None => "〉".to_string(),
|
||||
};
|
||||
|
||||
let prompt_multiline = match stack.get_env_var(engine_state, PROMPT_MULTILINE_INDICATOR) {
|
||||
Some(pm) => pm.into_string("", config),
|
||||
None => "::: ".to_string(),
|
||||
};
|
||||
|
||||
if is_perf_true {
|
||||
info!(
|
||||
"get_prompt_indicators {}:{}:{}",
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
prompt_indicator,
|
||||
prompt_vi_insert,
|
||||
prompt_vi_normal,
|
||||
prompt_multiline,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_prompt_string(
|
||||
prompt: &str,
|
||||
config: &Config,
|
||||
@ -102,28 +58,7 @@ fn get_prompt_string(
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::String { val: source, .. } => {
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
let (block, _) = parse(&mut working_set, None, source.as_bytes(), true, &[]);
|
||||
// Use eval_subexpression to force a redirection of output, so we can use everything in prompt
|
||||
let ret_val = eval_subexpression(
|
||||
engine_state,
|
||||
stack,
|
||||
&block,
|
||||
PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored
|
||||
)
|
||||
.ok();
|
||||
if is_perf_true {
|
||||
info!(
|
||||
"get_prompt_string (string) {}:{}:{}",
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
}
|
||||
|
||||
ret_val
|
||||
}
|
||||
Value::String { .. } => Some(PipelineData::Value(v.clone(), None)),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|pipeline_data| {
|
||||
@ -153,32 +88,60 @@ pub(crate) fn update_prompt<'prompt>(
|
||||
nu_prompt: &'prompt mut NushellPrompt,
|
||||
is_perf_true: bool,
|
||||
) -> &'prompt dyn Prompt {
|
||||
// get the other indicators
|
||||
let (
|
||||
prompt_indicator_string,
|
||||
prompt_vi_insert_string,
|
||||
prompt_vi_normal_string,
|
||||
prompt_multiline_string,
|
||||
) = get_prompt_indicators(config, engine_state, stack, is_perf_true);
|
||||
|
||||
let mut stack = stack.clone();
|
||||
|
||||
let left_prompt_string = get_prompt_string(
|
||||
PROMPT_COMMAND,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
let right_prompt_string = get_prompt_string(
|
||||
PROMPT_COMMAND_RIGHT,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
let prompt_indicator_string = get_prompt_string(
|
||||
PROMPT_INDICATOR,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
let prompt_multiline_string = get_prompt_string(
|
||||
PROMPT_MULTILINE_INDICATOR,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
let prompt_vi_insert_string = get_prompt_string(
|
||||
PROMPT_INDICATOR_VI_INSERT,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
let prompt_vi_normal_string = get_prompt_string(
|
||||
PROMPT_INDICATOR_VI_NORMAL,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
);
|
||||
|
||||
// apply the other indicators
|
||||
nu_prompt.update_all_prompt_strings(
|
||||
get_prompt_string(
|
||||
PROMPT_COMMAND,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
),
|
||||
get_prompt_string(
|
||||
PROMPT_COMMAND_RIGHT,
|
||||
config,
|
||||
engine_state,
|
||||
&mut stack,
|
||||
is_perf_true,
|
||||
),
|
||||
left_prompt_string,
|
||||
right_prompt_string,
|
||||
prompt_indicator_string,
|
||||
prompt_multiline_string,
|
||||
(prompt_vi_insert_string, prompt_vi_normal_string),
|
||||
|
@ -1,129 +1,507 @@
|
||||
use super::DescriptionMenu;
|
||||
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use nu_color_config::lookup_ansi_color_style;
|
||||
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
color_value_string, create_menus,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
extract_value, Config, IntoPipelineData, ParsedKeybinding, ParsedMenu, PipelineData,
|
||||
ShellError, Span, Value,
|
||||
};
|
||||
use reedline::{
|
||||
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
||||
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
|
||||
ColumnarMenu, EditCommand, Keybindings, ListMenu, Reedline, ReedlineEvent, ReedlineMenu,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Creates an input object for the completion menu based on the dictionary
|
||||
// stored in the config variable
|
||||
pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Reedline {
|
||||
let mut completion_menu = CompletionMenu::default();
|
||||
const DEFAULT_COMPLETION_MENU: &str = r#"
|
||||
{
|
||||
name: completion_menu
|
||||
only_buffer_difference: false
|
||||
marker: "| "
|
||||
type: {
|
||||
layout: columnar
|
||||
columns: 4
|
||||
col_width: 20
|
||||
col_padding: 2
|
||||
}
|
||||
style: {
|
||||
text: green,
|
||||
selected_text: green_reverse
|
||||
description_text: yellow
|
||||
}
|
||||
}"#;
|
||||
|
||||
completion_menu = match config
|
||||
.menu_config
|
||||
.get("columns")
|
||||
.and_then(|value| value.as_integer().ok())
|
||||
{
|
||||
Some(value) => completion_menu.with_columns(value as u16),
|
||||
None => completion_menu,
|
||||
};
|
||||
const DEFAULT_HISTORY_MENU: &str = r#"
|
||||
{
|
||||
name: history_menu
|
||||
only_buffer_difference: true
|
||||
marker: "? "
|
||||
type: {
|
||||
layout: list
|
||||
page_size: 10
|
||||
}
|
||||
style: {
|
||||
text: green,
|
||||
selected_text: green_reverse
|
||||
description_text: yellow
|
||||
}
|
||||
}"#;
|
||||
|
||||
completion_menu = completion_menu.with_column_width(
|
||||
config
|
||||
.menu_config
|
||||
.get("col_width")
|
||||
.and_then(|value| value.as_integer().ok())
|
||||
.map(|value| value as usize),
|
||||
);
|
||||
const DEFAULT_HELP_MENU: &str = r#"
|
||||
{
|
||||
name: help_menu
|
||||
only_buffer_difference: true
|
||||
marker: "? "
|
||||
type: {
|
||||
layout: description
|
||||
columns: 4
|
||||
col_width: 20
|
||||
col_padding: 2
|
||||
selection_rows: 4
|
||||
description_rows: 10
|
||||
}
|
||||
style: {
|
||||
text: green,
|
||||
selected_text: green_reverse
|
||||
description_text: yellow
|
||||
}
|
||||
}"#;
|
||||
|
||||
completion_menu = match config
|
||||
.menu_config
|
||||
.get("col_padding")
|
||||
.and_then(|value| value.as_integer().ok())
|
||||
{
|
||||
Some(value) => completion_menu.with_column_padding(value as usize),
|
||||
None => completion_menu,
|
||||
};
|
||||
// Adds all menus to line editor
|
||||
pub(crate) fn add_menus(
|
||||
mut line_editor: Reedline,
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Result<Reedline, ShellError> {
|
||||
line_editor = line_editor.clear_menus();
|
||||
|
||||
completion_menu = match config
|
||||
.menu_config
|
||||
.get("text_style")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => completion_menu.with_text_style(lookup_ansi_color_style(&value)),
|
||||
None => completion_menu,
|
||||
};
|
||||
for menu in &config.menus {
|
||||
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
|
||||
}
|
||||
|
||||
completion_menu = match config
|
||||
.menu_config
|
||||
.get("selected_text_style")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => completion_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
|
||||
None => completion_menu,
|
||||
};
|
||||
// Checking if the default menus have been added from the config file
|
||||
let default_menus = vec![
|
||||
("completion_menu", DEFAULT_COMPLETION_MENU),
|
||||
("history_menu", DEFAULT_HISTORY_MENU),
|
||||
("help_menu", DEFAULT_HELP_MENU),
|
||||
];
|
||||
|
||||
completion_menu = match config
|
||||
.menu_config
|
||||
.get("marker")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => completion_menu.with_marker(value),
|
||||
None => completion_menu,
|
||||
};
|
||||
for (name, definition) in default_menus {
|
||||
if !config
|
||||
.menus
|
||||
.iter()
|
||||
.any(|menu| menu.name.into_string("", config) == name)
|
||||
{
|
||||
let (block, _) = {
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
let (output, _) = parse(
|
||||
&mut working_set,
|
||||
Some(name), // format!("entry #{}", entry_num)
|
||||
definition.as_bytes(),
|
||||
true,
|
||||
&[],
|
||||
);
|
||||
|
||||
line_editor.with_menu(Box::new(completion_menu))
|
||||
(output, working_set.render())
|
||||
};
|
||||
|
||||
let mut temp_stack = Stack::new();
|
||||
let input = Value::nothing(Span::test_data()).into_pipeline_data();
|
||||
let res = eval_block(&engine_state, &mut temp_stack, &block, input, false, false)?;
|
||||
|
||||
if let PipelineData::Value(value, None) = res {
|
||||
for menu in create_menus(&value, config)? {
|
||||
line_editor =
|
||||
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(line_editor)
|
||||
}
|
||||
|
||||
// Creates an input object for the history menu based on the dictionary
|
||||
// stored in the config variable
|
||||
pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedline {
|
||||
let mut history_menu = HistoryMenu::default();
|
||||
fn add_menu(
|
||||
line_editor: Reedline,
|
||||
menu: &ParsedMenu,
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Result<Reedline, ShellError> {
|
||||
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
||||
let layout = extract_value("layout", cols, vals, span)?.into_string("", config);
|
||||
|
||||
history_menu = match config
|
||||
.history_config
|
||||
.get("page_size")
|
||||
.and_then(|value| value.as_integer().ok())
|
||||
{
|
||||
Some(value) => history_menu.with_page_size(value as usize),
|
||||
None => history_menu,
|
||||
};
|
||||
|
||||
history_menu = match config
|
||||
.history_config
|
||||
.get("selector")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => {
|
||||
let char = value.chars().next().unwrap_or('!');
|
||||
history_menu.with_selection_char(char)
|
||||
match layout.as_str() {
|
||||
"columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config),
|
||||
"list" => add_list_menu(line_editor, menu, engine_state, stack, config),
|
||||
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
|
||||
_ => Err(ShellError::UnsupportedConfigValue(
|
||||
"columnar, list or description".to_string(),
|
||||
menu.menu_type.into_abbreviated_string(config),
|
||||
menu.menu_type.span()?,
|
||||
)),
|
||||
}
|
||||
None => history_menu,
|
||||
};
|
||||
} else {
|
||||
Err(ShellError::UnsupportedConfigValue(
|
||||
"only record type".to_string(),
|
||||
menu.menu_type.into_abbreviated_string(config),
|
||||
menu.menu_type.span()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
history_menu = match config
|
||||
.history_config
|
||||
.get("text_style")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => history_menu.with_text_style(lookup_ansi_color_style(&value)),
|
||||
None => history_menu,
|
||||
macro_rules! add_style {
|
||||
// first arm match add!(1,2), add!(2,3) etc
|
||||
($name:expr, $cols: expr, $vals:expr, $span:expr, $config: expr, $menu:expr, $f:expr) => {
|
||||
$menu = match extract_value($name, $cols, $vals, $span) {
|
||||
Ok(text) => {
|
||||
let text = match text {
|
||||
Value::String { val, .. } => val.clone(),
|
||||
Value::Record { cols, vals, span } => {
|
||||
color_value_string(span, cols, vals, $config).into_string("", $config)
|
||||
}
|
||||
_ => "green".to_string(),
|
||||
};
|
||||
let style = lookup_ansi_color_style(&text);
|
||||
$f($menu, style)
|
||||
}
|
||||
Err(_) => $menu,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
history_menu = match config
|
||||
.history_config
|
||||
.get("selected_text_style")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => history_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
|
||||
None => history_menu,
|
||||
};
|
||||
// Adds a columnar menu to the editor engine
|
||||
pub(crate) fn add_columnar_menu(
|
||||
line_editor: Reedline,
|
||||
menu: &ParsedMenu,
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Result<Reedline, ShellError> {
|
||||
let name = menu.name.into_string("", config);
|
||||
let mut columnar_menu = ColumnarMenu::default().with_name(&name);
|
||||
|
||||
history_menu = match config
|
||||
.history_config
|
||||
.get("marker")
|
||||
.and_then(|value| value.as_string().ok())
|
||||
{
|
||||
Some(value) => history_menu.with_marker(value),
|
||||
None => history_menu,
|
||||
};
|
||||
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
||||
columnar_menu = match extract_value("columns", cols, vals, span) {
|
||||
Ok(columns) => {
|
||||
let columns = columns.as_integer()?;
|
||||
columnar_menu.with_columns(columns as u16)
|
||||
}
|
||||
Err(_) => columnar_menu,
|
||||
};
|
||||
|
||||
line_editor.with_menu(Box::new(history_menu))
|
||||
columnar_menu = match extract_value("col_width", cols, vals, span) {
|
||||
Ok(col_width) => {
|
||||
let col_width = col_width.as_integer()?;
|
||||
columnar_menu.with_column_width(Some(col_width as usize))
|
||||
}
|
||||
Err(_) => columnar_menu.with_column_width(None),
|
||||
};
|
||||
|
||||
columnar_menu = match extract_value("col_padding", cols, vals, span) {
|
||||
Ok(col_padding) => {
|
||||
let col_padding = col_padding.as_integer()?;
|
||||
columnar_menu.with_column_padding(col_padding as usize)
|
||||
}
|
||||
Err(_) => columnar_menu,
|
||||
};
|
||||
}
|
||||
|
||||
if let Value::Record { cols, vals, span } = &menu.style {
|
||||
add_style!(
|
||||
"text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
columnar_menu,
|
||||
ColumnarMenu::with_text_style
|
||||
);
|
||||
add_style!(
|
||||
"selected_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
columnar_menu,
|
||||
ColumnarMenu::with_selected_text_style
|
||||
);
|
||||
add_style!(
|
||||
"description_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
columnar_menu,
|
||||
ColumnarMenu::with_description_text_style
|
||||
);
|
||||
}
|
||||
|
||||
let marker = menu.marker.into_string("", config);
|
||||
columnar_menu = columnar_menu.with_marker(marker);
|
||||
|
||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
|
||||
|
||||
match &menu.source {
|
||||
Value::Nothing { .. } => {
|
||||
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
|
||||
}
|
||||
Value::Block {
|
||||
val,
|
||||
captures,
|
||||
span,
|
||||
} => {
|
||||
let menu_completer = NuMenuCompleter::new(
|
||||
*val,
|
||||
*span,
|
||||
stack.captures_to_stack(captures),
|
||||
engine_state,
|
||||
only_buffer_difference,
|
||||
);
|
||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
||||
menu: Box::new(columnar_menu),
|
||||
completer: Box::new(menu_completer),
|
||||
}))
|
||||
}
|
||||
_ => Err(ShellError::UnsupportedConfigValue(
|
||||
"block or omitted value".to_string(),
|
||||
menu.source.into_abbreviated_string(config),
|
||||
menu.source.span()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a search menu to the line editor
|
||||
pub(crate) fn add_list_menu(
|
||||
line_editor: Reedline,
|
||||
menu: &ParsedMenu,
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Result<Reedline, ShellError> {
|
||||
let name = menu.name.into_string("", config);
|
||||
let mut list_menu = ListMenu::default().with_name(&name);
|
||||
|
||||
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
||||
list_menu = match extract_value("page_size", cols, vals, span) {
|
||||
Ok(page_size) => {
|
||||
let page_size = page_size.as_integer()?;
|
||||
list_menu.with_page_size(page_size as usize)
|
||||
}
|
||||
Err(_) => list_menu,
|
||||
};
|
||||
}
|
||||
|
||||
if let Value::Record { cols, vals, span } = &menu.style {
|
||||
add_style!(
|
||||
"text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
list_menu,
|
||||
ListMenu::with_text_style
|
||||
);
|
||||
add_style!(
|
||||
"selected_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
list_menu,
|
||||
ListMenu::with_selected_text_style
|
||||
);
|
||||
add_style!(
|
||||
"description_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
list_menu,
|
||||
ListMenu::with_description_text_style
|
||||
);
|
||||
}
|
||||
|
||||
let marker = menu.marker.into_string("", config);
|
||||
list_menu = list_menu.with_marker(marker);
|
||||
|
||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
|
||||
|
||||
match &menu.source {
|
||||
Value::Nothing { .. } => {
|
||||
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
|
||||
}
|
||||
Value::Block {
|
||||
val,
|
||||
captures,
|
||||
span,
|
||||
} => {
|
||||
let menu_completer = NuMenuCompleter::new(
|
||||
*val,
|
||||
*span,
|
||||
stack.captures_to_stack(captures),
|
||||
engine_state,
|
||||
only_buffer_difference,
|
||||
);
|
||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
||||
menu: Box::new(list_menu),
|
||||
completer: Box::new(menu_completer),
|
||||
}))
|
||||
}
|
||||
_ => Err(ShellError::UnsupportedConfigValue(
|
||||
"block or omitted value".to_string(),
|
||||
menu.source.into_abbreviated_string(config),
|
||||
menu.source.span()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a description menu to the line editor
|
||||
pub(crate) fn add_description_menu(
|
||||
line_editor: Reedline,
|
||||
menu: &ParsedMenu,
|
||||
engine_state: Arc<EngineState>,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Result<Reedline, ShellError> {
|
||||
let name = menu.name.into_string("", config);
|
||||
let mut description_menu = DescriptionMenu::default().with_name(&name);
|
||||
|
||||
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
||||
description_menu = match extract_value("columns", cols, vals, span) {
|
||||
Ok(columns) => {
|
||||
let columns = columns.as_integer()?;
|
||||
description_menu.with_columns(columns as u16)
|
||||
}
|
||||
Err(_) => description_menu,
|
||||
};
|
||||
|
||||
description_menu = match extract_value("col_width", cols, vals, span) {
|
||||
Ok(col_width) => {
|
||||
let col_width = col_width.as_integer()?;
|
||||
description_menu.with_column_width(Some(col_width as usize))
|
||||
}
|
||||
Err(_) => description_menu.with_column_width(None),
|
||||
};
|
||||
|
||||
description_menu = match extract_value("col_padding", cols, vals, span) {
|
||||
Ok(col_padding) => {
|
||||
let col_padding = col_padding.as_integer()?;
|
||||
description_menu.with_column_padding(col_padding as usize)
|
||||
}
|
||||
Err(_) => description_menu,
|
||||
};
|
||||
|
||||
description_menu = match extract_value("selection_rows", cols, vals, span) {
|
||||
Ok(selection_rows) => {
|
||||
let selection_rows = selection_rows.as_integer()?;
|
||||
description_menu.with_selection_rows(selection_rows as u16)
|
||||
}
|
||||
Err(_) => description_menu,
|
||||
};
|
||||
|
||||
description_menu = match extract_value("description_rows", cols, vals, span) {
|
||||
Ok(description_rows) => {
|
||||
let description_rows = description_rows.as_integer()?;
|
||||
description_menu.with_description_rows(description_rows as usize)
|
||||
}
|
||||
Err(_) => description_menu,
|
||||
};
|
||||
}
|
||||
|
||||
if let Value::Record { cols, vals, span } = &menu.style {
|
||||
add_style!(
|
||||
"text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
description_menu,
|
||||
DescriptionMenu::with_text_style
|
||||
);
|
||||
add_style!(
|
||||
"selected_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
description_menu,
|
||||
DescriptionMenu::with_selected_text_style
|
||||
);
|
||||
add_style!(
|
||||
"description_text",
|
||||
cols,
|
||||
vals,
|
||||
span,
|
||||
config,
|
||||
description_menu,
|
||||
DescriptionMenu::with_description_text_style
|
||||
);
|
||||
}
|
||||
|
||||
let marker = menu.marker.into_string("", config);
|
||||
description_menu = description_menu.with_marker(marker);
|
||||
|
||||
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
||||
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
|
||||
|
||||
match &menu.source {
|
||||
Value::Nothing { .. } => {
|
||||
let completer = Box::new(NuHelpCompleter::new(engine_state));
|
||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
||||
menu: Box::new(description_menu),
|
||||
completer,
|
||||
}))
|
||||
}
|
||||
Value::Block {
|
||||
val,
|
||||
captures,
|
||||
span,
|
||||
} => {
|
||||
let menu_completer = NuMenuCompleter::new(
|
||||
*val,
|
||||
*span,
|
||||
stack.captures_to_stack(captures),
|
||||
engine_state,
|
||||
only_buffer_difference,
|
||||
);
|
||||
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
||||
menu: Box::new(description_menu),
|
||||
completer: Box::new(menu_completer),
|
||||
}))
|
||||
}
|
||||
_ => Err(ShellError::UnsupportedConfigValue(
|
||||
"block or omitted value".to_string(),
|
||||
menu.source.into_abbreviated_string(config),
|
||||
menu.source.span()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
||||
// Completer menu keybindings
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::NONE,
|
||||
KeyCode::Tab,
|
||||
ReedlineEvent::UntilFound(vec![
|
||||
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||
ReedlineEvent::MenuNext,
|
||||
]),
|
||||
);
|
||||
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::SHIFT,
|
||||
KeyCode::BackTab,
|
||||
ReedlineEvent::MenuPrevious,
|
||||
);
|
||||
|
||||
// History menu keybinding
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('x'),
|
||||
@ -142,19 +520,11 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
||||
]),
|
||||
);
|
||||
|
||||
// Help menu keybinding
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::NONE,
|
||||
KeyCode::Tab,
|
||||
ReedlineEvent::UntilFound(vec![
|
||||
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||
ReedlineEvent::MenuNext,
|
||||
]),
|
||||
);
|
||||
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::SHIFT,
|
||||
KeyCode::BackTab,
|
||||
ReedlineEvent::MenuPrevious,
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('q'),
|
||||
ReedlineEvent::Menu("help_menu".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
@ -173,6 +543,15 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
||||
let mut insert_keybindings = default_vi_insert_keybindings();
|
||||
let mut normal_keybindings = default_vi_normal_keybindings();
|
||||
|
||||
match config.edit_mode.as_str() {
|
||||
"emacs" => {
|
||||
add_menu_keybindings(&mut emacs_keybindings);
|
||||
}
|
||||
_ => {
|
||||
add_menu_keybindings(&mut insert_keybindings);
|
||||
add_menu_keybindings(&mut normal_keybindings);
|
||||
}
|
||||
}
|
||||
for keybinding in parsed_keybindings {
|
||||
add_keybinding(
|
||||
&keybinding.mode,
|
||||
@ -185,20 +564,11 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
|
||||
}
|
||||
|
||||
match config.edit_mode.as_str() {
|
||||
"emacs" => {
|
||||
add_menu_keybindings(&mut emacs_keybindings);
|
||||
|
||||
Ok(KeybindingsMode::Emacs(emacs_keybindings))
|
||||
}
|
||||
_ => {
|
||||
add_menu_keybindings(&mut insert_keybindings);
|
||||
add_menu_keybindings(&mut normal_keybindings);
|
||||
|
||||
Ok(KeybindingsMode::Vi {
|
||||
insert_keybindings,
|
||||
normal_keybindings,
|
||||
})
|
||||
}
|
||||
"emacs" => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
|
||||
_ => Ok(KeybindingsMode::Vi {
|
||||
insert_keybindings,
|
||||
normal_keybindings,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,10 +698,11 @@ fn add_parsed_keybinding(
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let event = parse_event(&keybinding.event, config)?;
|
||||
|
||||
keybindings.add_binding(modifier, keycode, event);
|
||||
if let Some(event) = parse_event(&keybinding.event, config)? {
|
||||
keybindings.add_binding(modifier, keycode, event);
|
||||
} else {
|
||||
keybindings.remove_binding(modifier, keycode);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -356,7 +727,7 @@ impl<'config> EventType<'config> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellError> {
|
||||
fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>, ShellError> {
|
||||
match value {
|
||||
Value::Record { cols, vals, span } => {
|
||||
match EventType::try_from_columns(cols, vals, span)? {
|
||||
@ -366,7 +737,8 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
|
||||
vals,
|
||||
config,
|
||||
span,
|
||||
),
|
||||
)
|
||||
.map(Some),
|
||||
EventType::Edit(value) => {
|
||||
let edit = edit_from_record(
|
||||
value.into_string("", config).to_lowercase().as_str(),
|
||||
@ -375,16 +747,26 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
|
||||
config,
|
||||
span,
|
||||
)?;
|
||||
Ok(ReedlineEvent::Edit(vec![edit]))
|
||||
Ok(Some(ReedlineEvent::Edit(vec![edit])))
|
||||
}
|
||||
EventType::Until(value) => match value {
|
||||
Value::List { vals, .. } => {
|
||||
let events = vals
|
||||
.iter()
|
||||
.map(|value| parse_event(value, config))
|
||||
.map(|value| match parse_event(value, config) {
|
||||
Ok(inner) => match inner {
|
||||
None => Err(ShellError::UnsupportedConfigValue(
|
||||
"List containing valid events".to_string(),
|
||||
"Nothing value (null)".to_string(),
|
||||
value.span()?,
|
||||
)),
|
||||
Some(event) => Ok(event),
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
||||
|
||||
Ok(ReedlineEvent::UntilFound(events))
|
||||
Ok(Some(ReedlineEvent::UntilFound(events)))
|
||||
}
|
||||
v => Err(ShellError::UnsupportedConfigValue(
|
||||
"list of events".to_string(),
|
||||
@ -397,13 +779,24 @@ fn parse_event(value: &Value, config: &Config) -> Result<ReedlineEvent, ShellErr
|
||||
Value::List { vals, .. } => {
|
||||
let events = vals
|
||||
.iter()
|
||||
.map(|value| parse_event(value, config))
|
||||
.map(|value| match parse_event(value, config) {
|
||||
Ok(inner) => match inner {
|
||||
None => Err(ShellError::UnsupportedConfigValue(
|
||||
"List containing valid events".to_string(),
|
||||
"Nothing value (null)".to_string(),
|
||||
value.span()?,
|
||||
)),
|
||||
Some(event) => Ok(event),
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
||||
|
||||
Ok(ReedlineEvent::Multiple(events))
|
||||
Ok(Some(ReedlineEvent::Multiple(events)))
|
||||
}
|
||||
Value::Nothing { .. } => Ok(None),
|
||||
v => Err(ShellError::UnsupportedConfigValue(
|
||||
"record or list of records".to_string(),
|
||||
"record or list of records, null to unbind key".to_string(),
|
||||
v.into_abbreviated_string(config),
|
||||
v.span()?,
|
||||
)),
|
||||
@ -595,7 +988,7 @@ mod test {
|
||||
let config = Config::default();
|
||||
|
||||
let parsed_event = parse_event(&event, &config).unwrap();
|
||||
assert_eq!(parsed_event, ReedlineEvent::Enter);
|
||||
assert_eq!(parsed_event, Some(ReedlineEvent::Enter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -618,7 +1011,10 @@ mod test {
|
||||
let config = Config::default();
|
||||
|
||||
let parsed_event = parse_event(&event, &config).unwrap();
|
||||
assert_eq!(parsed_event, ReedlineEvent::Edit(vec![EditCommand::Clear]));
|
||||
assert_eq!(
|
||||
parsed_event,
|
||||
Some(ReedlineEvent::Edit(vec![EditCommand::Clear]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -649,7 +1045,7 @@ mod test {
|
||||
let parsed_event = parse_event(&event, &config).unwrap();
|
||||
assert_eq!(
|
||||
parsed_event,
|
||||
ReedlineEvent::Menu("history_menu".to_string())
|
||||
Some(ReedlineEvent::Menu("history_menu".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
@ -708,10 +1104,10 @@ mod test {
|
||||
let parsed_event = parse_event(&event, &config).unwrap();
|
||||
assert_eq!(
|
||||
parsed_event,
|
||||
ReedlineEvent::UntilFound(vec![
|
||||
Some(ReedlineEvent::UntilFound(vec![
|
||||
ReedlineEvent::Menu("history_menu".to_string()),
|
||||
ReedlineEvent::Enter,
|
||||
])
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
@ -759,10 +1155,10 @@ mod test {
|
||||
let parsed_event = parse_event(&event, &config).unwrap();
|
||||
assert_eq!(
|
||||
parsed_event,
|
||||
ReedlineEvent::Multiple(vec![
|
||||
Some(ReedlineEvent::Multiple(vec![
|
||||
ReedlineEvent::Menu("history_menu".to_string()),
|
||||
ReedlineEvent::Enter,
|
||||
])
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
use crate::reedline_config::{add_completion_menu, add_history_menu};
|
||||
use crate::reedline_config::add_menus;
|
||||
use crate::{completions::NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
|
||||
use crate::{prompt_update, reedline_config};
|
||||
use crate::{
|
||||
reedline_config::KeybindingsMode,
|
||||
util::{eval_source, report_error},
|
||||
};
|
||||
use crate::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
|
||||
use log::info;
|
||||
use log::trace;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
@ -27,48 +27,11 @@ pub fn evaluate_repl(
|
||||
history_path: Option<PathBuf>,
|
||||
is_perf_true: bool,
|
||||
) -> Result<()> {
|
||||
// use crate::logger::{configure, logger};
|
||||
use reedline::{FileBackedHistory, Reedline, Signal};
|
||||
|
||||
let mut entry_num = 0;
|
||||
|
||||
let mut nu_prompt = NushellPrompt::new();
|
||||
// let mut stack = nu_protocol::engine::Stack::new();
|
||||
|
||||
// First, set up env vars as strings only
|
||||
// gather_parent_env_vars(engine_state);
|
||||
|
||||
// Set up our initial config to start from
|
||||
// stack.vars.insert(
|
||||
// CONFIG_VARIABLE_ID,
|
||||
// Value::Record {
|
||||
// cols: vec![],
|
||||
// vals: vec![],
|
||||
// span: Span::new(0, 0),
|
||||
// },
|
||||
// );
|
||||
|
||||
if is_perf_true {
|
||||
info!("read_plugin_file {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
|
||||
// #[cfg(feature = "plugin")]
|
||||
// config_files::read_plugin_file(engine_state, &mut stack, is_perf_true);
|
||||
//
|
||||
// if is_perf_true {
|
||||
// info!("read_config_file {}:{}:{}", file!(), line!(), column!());
|
||||
// }
|
||||
//
|
||||
// config_files::read_config_file(engine_state, &mut stack, config_file, is_perf_true);
|
||||
// let history_path = config_files::create_history_path();
|
||||
|
||||
// logger(|builder| {
|
||||
// configure(&config.log_level, builder)?;
|
||||
// // trace_filters(self, builder)?;
|
||||
// // debug_filters(self, builder)?;
|
||||
|
||||
// Ok(())
|
||||
// })?;
|
||||
|
||||
if is_perf_true {
|
||||
info!(
|
||||
@ -85,10 +48,6 @@ pub fn evaluate_repl(
|
||||
report_error(&working_set, &e);
|
||||
}
|
||||
|
||||
// Make a note of the exceptions we see for externals that look like math expressions
|
||||
let exceptions = crate::util::external_exceptions(engine_state, stack);
|
||||
engine_state.external_exceptions = exceptions;
|
||||
|
||||
// seed env vars
|
||||
stack.add_env_var(
|
||||
"CMD_DURATION_MS".into(),
|
||||
@ -106,6 +65,45 @@ pub fn evaluate_repl(
|
||||
},
|
||||
);
|
||||
|
||||
if is_perf_true {
|
||||
info!(
|
||||
"load config initially {}:{}:{}",
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
}
|
||||
|
||||
// Get the config once for the history `max_history_size`
|
||||
// Updating that will not be possible in one session
|
||||
let mut config = match stack.get_config() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
|
||||
report_error(&working_set, &e);
|
||||
Config::default()
|
||||
}
|
||||
};
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup reedline {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
let mut line_editor = Reedline::create();
|
||||
if let Some(history_path) = history_path.as_deref() {
|
||||
if is_perf_true {
|
||||
info!("setup history {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
let history = Box::new(
|
||||
FileBackedHistory::with_file(
|
||||
config.max_history_size as usize,
|
||||
history_path.to_path_buf(),
|
||||
)
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
line_editor = line_editor.with_history(history);
|
||||
};
|
||||
|
||||
loop {
|
||||
if is_perf_true {
|
||||
info!(
|
||||
@ -116,7 +114,7 @@ pub fn evaluate_repl(
|
||||
);
|
||||
}
|
||||
|
||||
let config = match stack.get_config() {
|
||||
config = match stack.get_config() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
@ -126,17 +124,22 @@ pub fn evaluate_repl(
|
||||
}
|
||||
};
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup colors {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
|
||||
let color_hm = get_color_config(&config);
|
||||
|
||||
//Reset the ctrl-c handler
|
||||
if let Some(ctrlc) = &mut engine_state.ctrlc {
|
||||
ctrlc.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup line editor {}:{}:{}", file!(), line!(), column!());
|
||||
info!("update reedline {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
|
||||
let mut line_editor = Reedline::create()
|
||||
.into_diagnostic()?
|
||||
let engine_reference = std::sync::Arc::new(engine_state.clone());
|
||||
line_editor = line_editor
|
||||
.with_highlighter(Box::new(NuHighlighter {
|
||||
engine_state: engine_state.clone(),
|
||||
config: config.clone(),
|
||||
@ -146,59 +149,38 @@ pub fn evaluate_repl(
|
||||
engine_state: engine_state.clone(),
|
||||
}))
|
||||
.with_completer(Box::new(NuCompleter::new(
|
||||
engine_state.clone(),
|
||||
engine_reference.clone(),
|
||||
stack.clone(),
|
||||
stack.vars.get(&CONFIG_VARIABLE_ID).cloned(),
|
||||
)))
|
||||
.with_quick_completions(config.quick_completions)
|
||||
.with_partial_completions(config.partial_completions)
|
||||
.with_ansi_colors(config.use_ansi_coloring);
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup reedline {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
|
||||
line_editor = add_completion_menu(line_editor, &config);
|
||||
line_editor = add_history_menu(line_editor, &config);
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup colors {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
//FIXME: if config.use_ansi_coloring is false then we should
|
||||
// turn off the hinter but I don't see any way to do that yet.
|
||||
|
||||
let color_hm = get_color_config(&config);
|
||||
|
||||
if is_perf_true {
|
||||
info!(
|
||||
"setup history and hinter {}:{}:{}",
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
}
|
||||
|
||||
line_editor = if let Some(history_path) = history_path.clone() {
|
||||
let history = std::fs::read_to_string(&history_path);
|
||||
if history.is_ok() {
|
||||
line_editor
|
||||
.with_hinter(Box::new(
|
||||
DefaultHinter::default().with_style(color_hm["hints"]),
|
||||
))
|
||||
.with_history(Box::new(
|
||||
FileBackedHistory::with_file(
|
||||
config.max_history_size as usize,
|
||||
history_path.clone(),
|
||||
)
|
||||
.into_diagnostic()?,
|
||||
))
|
||||
.into_diagnostic()?
|
||||
} else {
|
||||
line_editor
|
||||
}
|
||||
line_editor = if config.use_ansi_coloring {
|
||||
line_editor.with_hinter(Box::new(
|
||||
DefaultHinter::default().with_style(color_hm["hints"]),
|
||||
))
|
||||
} else {
|
||||
line_editor
|
||||
line_editor.disable_hints()
|
||||
};
|
||||
|
||||
line_editor = match add_menus(line_editor, engine_reference, stack, &config) {
|
||||
Ok(line_editor) => line_editor,
|
||||
Err(e) => {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, &e);
|
||||
Reedline::create()
|
||||
}
|
||||
};
|
||||
|
||||
if config.sync_history_on_enter {
|
||||
if is_perf_true {
|
||||
info!("sync history {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
line_editor.sync_history().into_diagnostic()?;
|
||||
}
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup keybindings {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
@ -338,10 +320,6 @@ pub fn evaluate_repl(
|
||||
let _ = std::env::set_current_dir(path);
|
||||
engine_state.env_vars.insert("PWD".into(), cwd);
|
||||
}
|
||||
|
||||
// Make a note of the exceptions we see for externals that look like math expressions
|
||||
let exceptions = crate::util::external_exceptions(engine_state, stack);
|
||||
engine_state.external_exceptions = exceptions;
|
||||
}
|
||||
Ok(Signal::CtrlC) => {
|
||||
// `Reedline` clears the line content. New prompt is shown
|
||||
|
@ -98,19 +98,6 @@ pub fn print_pipeline_data(
|
||||
// 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.
|
||||
pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
|
||||
// Some helper functions
|
||||
fn get_surround_char(s: &str) -> Option<char> {
|
||||
if s.contains('"') {
|
||||
if s.contains('\'') {
|
||||
None
|
||||
} else {
|
||||
Some('\'')
|
||||
}
|
||||
} else {
|
||||
Some('\'')
|
||||
}
|
||||
}
|
||||
|
||||
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(
|
||||
@ -122,32 +109,14 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
|
||||
);
|
||||
}
|
||||
|
||||
fn put_env_to_fake_file(
|
||||
name: &str,
|
||||
val: &str,
|
||||
fake_env_file: &mut String,
|
||||
engine_state: &EngineState,
|
||||
) {
|
||||
let (c_name, c_val) =
|
||||
if let (Some(cn), Some(cv)) = (get_surround_char(name), get_surround_char(val)) {
|
||||
(cn, cv)
|
||||
} else {
|
||||
// environment variable with its name or value containing both ' and " is ignored
|
||||
report_capture_error(
|
||||
engine_state,
|
||||
&format!("{}={}", name, val),
|
||||
"Name or value should not contain both ' and \" at the same time.",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
fake_env_file.push(c_name);
|
||||
fn put_env_to_fake_file(name: &str, val: &str, fake_env_file: &mut String) {
|
||||
fake_env_file.push('`');
|
||||
fake_env_file.push_str(name);
|
||||
fake_env_file.push(c_name);
|
||||
fake_env_file.push('`');
|
||||
fake_env_file.push('=');
|
||||
fake_env_file.push(c_val);
|
||||
fake_env_file.push('`');
|
||||
fake_env_file.push_str(val);
|
||||
fake_env_file.push(c_val);
|
||||
fake_env_file.push('`');
|
||||
fake_env_file.push('\n');
|
||||
}
|
||||
|
||||
@ -157,12 +126,7 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
|
||||
if std::env::var("PWD").is_err() {
|
||||
match std::env::current_dir() {
|
||||
Ok(cwd) => {
|
||||
put_env_to_fake_file(
|
||||
"PWD",
|
||||
&cwd.to_string_lossy(),
|
||||
&mut fake_env_file,
|
||||
engine_state,
|
||||
);
|
||||
put_env_to_fake_file("PWD", &cwd.to_string_lossy(), &mut fake_env_file);
|
||||
}
|
||||
Err(e) => {
|
||||
// Could not capture current working directory
|
||||
@ -180,7 +144,7 @@ pub fn gather_parent_env_vars(engine_state: &mut EngineState) {
|
||||
|
||||
// Write all the env vars into a fake file
|
||||
for (name, val) in std::env::vars() {
|
||||
put_env_to_fake_file(&name, &val, &mut fake_env_file, engine_state);
|
||||
put_env_to_fake_file(&name, &val, &mut fake_env_file);
|
||||
}
|
||||
|
||||
// Lex the fake file, assign spans to all environment variables and add them
|
||||
@ -290,6 +254,7 @@ pub fn eval_source(
|
||||
&[],
|
||||
);
|
||||
if let Some(err) = err {
|
||||
set_last_exit_code(stack, 1);
|
||||
report_error(&working_set, &err);
|
||||
return false;
|
||||
}
|
||||
@ -317,22 +282,10 @@ pub fn eval_source(
|
||||
if let Some(exit_code) = exit_code.take().and_then(|it| it.last()) {
|
||||
stack.add_env_var("LAST_EXIT_CODE".to_string(), exit_code);
|
||||
} else {
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".to_string(),
|
||||
Value::Int {
|
||||
val: 0,
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
set_last_exit_code(stack, 0);
|
||||
}
|
||||
} else {
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".to_string(),
|
||||
Value::Int {
|
||||
val: 0,
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
set_last_exit_code(stack, 0);
|
||||
}
|
||||
|
||||
if let Err(err) = print_pipeline_data(pipeline_data, engine_state, stack) {
|
||||
@ -350,13 +303,7 @@ pub fn eval_source(
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".to_string(),
|
||||
Value::Int {
|
||||
val: 1,
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
set_last_exit_code(stack, 1);
|
||||
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
|
||||
@ -369,96 +316,14 @@ pub fn eval_source(
|
||||
true
|
||||
}
|
||||
|
||||
fn seems_like_number(bytes: &[u8]) -> bool {
|
||||
if bytes.is_empty() {
|
||||
false
|
||||
} else {
|
||||
let b = bytes[0];
|
||||
|
||||
b == b'0'
|
||||
|| b == b'1'
|
||||
|| b == b'2'
|
||||
|| b == b'3'
|
||||
|| b == b'4'
|
||||
|| b == b'5'
|
||||
|| b == b'6'
|
||||
|| b == b'7'
|
||||
|| b == b'8'
|
||||
|| b == b'9'
|
||||
|| b == b'('
|
||||
|| b == b'{'
|
||||
|| b == b'['
|
||||
|| b == b'$'
|
||||
|| b == b'"'
|
||||
|| b == b'\''
|
||||
|| b == b'-'
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds externals that have names that look like math expressions
|
||||
pub fn external_exceptions(engine_state: &EngineState, stack: &Stack) -> Vec<Vec<u8>> {
|
||||
let mut executables = vec![];
|
||||
|
||||
if let Some(path) = stack.get_env_var(engine_state, "PATH") {
|
||||
match path {
|
||||
Value::List { vals, .. } => {
|
||||
for val in vals {
|
||||
let path = val.as_string();
|
||||
|
||||
if let Ok(path) = path {
|
||||
if let Ok(mut contents) = std::fs::read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if is_executable::is_executable(&item.path()) {
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
if seems_like_number(name.as_bytes()) {
|
||||
let name = name.as_bytes().to_vec();
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = item.path().file_stem() {
|
||||
let name = name.to_string_lossy();
|
||||
if seems_like_number(name.as_bytes()) {
|
||||
let name = name.as_bytes().to_vec();
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::String { val, .. } => {
|
||||
for path in std::env::split_paths(&val) {
|
||||
let path = path.to_string_lossy().to_string();
|
||||
|
||||
if let Ok(mut contents) = std::fs::read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if is_executable::is_executable(&item.path()) {
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
if seems_like_number(name.as_bytes()) {
|
||||
let name = name.as_bytes().to_vec();
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
if let Some(name) = item.path().file_stem() {
|
||||
let name = name.to_string_lossy();
|
||||
if seems_like_number(name.as_bytes()) {
|
||||
let name = name.as_bytes().to_vec();
|
||||
executables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
executables
|
||||
fn set_last_exit_code(stack: &mut Stack, exit_code: i64) {
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".to_string(),
|
||||
Value::Int {
|
||||
val: exit_code,
|
||||
span: Span { start: 0, end: 0 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn report_error(
|
||||
|
@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "nu-color-config"
|
||||
version = "0.60.0"
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Color configuration code used by Nushell"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-color-config"
|
||||
version = "0.61.0"
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
|
||||
nu-json = { path = "../nu-json", version = "0.60.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.60.0" }
|
||||
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-json = { path = "../nu-json", version = "0.61.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.61.0" }
|
||||
serde = { version="1.0.123", features=["derive"] }
|
||||
|
21
crates/nu-color-config/LICENSE
Normal file
21
crates/nu-color-config/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2022 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.
|
@ -1,27 +1,29 @@
|
||||
[package]
|
||||
name = "nu-command"
|
||||
version = "0.60.0"
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Nushell's built-in commands"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-command"
|
||||
version = "0.61.0"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.60.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.60.0" }
|
||||
nu-glob = { path = "../nu-glob", version = "0.60.0" }
|
||||
nu-json = { path = "../nu-json", version = "0.60.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.60.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.60.0" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.60.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.0" }
|
||||
nu-system = { path = "../nu-system", version = "0.60.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.60.0" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.60.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.60.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.60.0" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.61.0" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.61.0" }
|
||||
nu-glob = { path = "../nu-glob", version = "0.61.0" }
|
||||
nu-json = { path = "../nu-json", version = "0.61.0" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.61.0" }
|
||||
nu-path = { path = "../nu-path", version = "0.61.0" }
|
||||
nu-pretty-hex = { path = "../nu-pretty-hex", version = "0.61.0" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.61.0" }
|
||||
nu-system = { path = "../nu-system", version = "0.61.0" }
|
||||
nu-table = { path = "../nu-table", version = "0.61.0" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.61.0" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.61.0" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.61.0" }
|
||||
nu-ansi-term = "0.45.1"
|
||||
|
||||
# Potential dependencies for extras
|
||||
base64 = "0.13.0"
|
||||
@ -31,13 +33,14 @@ chrono = { version = "0.4.19", features = ["serde"] }
|
||||
chrono-humanize = "0.2.1"
|
||||
chrono-tz = "0.6.0"
|
||||
crossterm = "0.23.0"
|
||||
csv = "1.1.3"
|
||||
csv = "1.1.6"
|
||||
dialoguer = "0.9.0"
|
||||
digest = "0.10.0"
|
||||
dtparse = "1.2.0"
|
||||
eml-parser = "0.1.0"
|
||||
encoding_rs = "0.8.30"
|
||||
filesize = "0.2.0"
|
||||
filetime = "0.2.15"
|
||||
fs_extra = "1.2.0"
|
||||
htmlescape = "0.3.1"
|
||||
ical = "0.7.0"
|
||||
@ -64,25 +67,29 @@ serde_ini = "0.2.0"
|
||||
serde_urlencoded = "0.7.0"
|
||||
serde_yaml = "0.8.16"
|
||||
sha2 = "0.10.0"
|
||||
shadow-rs = "0.8.1"
|
||||
shadow-rs = "0.11.0"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
sysinfo = "0.23.5"
|
||||
terminal_size = "0.1.17"
|
||||
thiserror = "1.0.29"
|
||||
titlecase = "1.1.0"
|
||||
toml = "0.5.8"
|
||||
trash = { version = "2.0.2", optional = true }
|
||||
unicode-segmentation = "1.8.0"
|
||||
url = "2.2.1"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
||||
which = { version = "4.2.2", optional = true }
|
||||
reedline = "0.3.0"
|
||||
reedline = { version = "0.4.0", features = ["bashisms"]}
|
||||
wax = { version = "0.4.0", features = ["diagnostics"] }
|
||||
zip = { version="0.5.9", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
umask = "1.0.0"
|
||||
users = "0.11.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies.trash]
|
||||
version = "2.0.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.polars]
|
||||
version = "0.20.0"
|
||||
optional = true
|
||||
@ -95,11 +102,12 @@ features = [
|
||||
|
||||
[features]
|
||||
trash-support = ["trash"]
|
||||
which-support = ["which"]
|
||||
plugin = ["nu-parser/plugin"]
|
||||
dataframe = ["polars", "num"]
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = "0.8.1"
|
||||
shadow-rs = "0.11.0"
|
||||
|
||||
[dev-dependencies]
|
||||
hamcrest2 = "0.3.0"
|
||||
|
21
crates/nu-command/LICENSE
Normal file
21
crates/nu-command/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2022 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.
|
@ -14,7 +14,7 @@ impl Command for Fmt {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"format numbers"
|
||||
"Format a number"
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
@ -23,7 +23,7 @@ impl Command for Fmt {
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "format numbers",
|
||||
description: "Get a record containing multiple formats for the number 42",
|
||||
example: "42 | fmt",
|
||||
result: Some(Value::Record {
|
||||
cols: vec![
|
||||
|
@ -136,10 +136,11 @@ fn string_to_boolean(s: &str, span: Span) -> Result<bool, ShellError> {
|
||||
let val = o.parse::<f64>();
|
||||
match val {
|
||||
Ok(f) => Ok(f.abs() >= f64::EPSILON),
|
||||
Err(_) => Err(ShellError::CantConvert(
|
||||
Err(_) => Err(ShellError::CantConvertWithHelp(
|
||||
"boolean".to_string(),
|
||||
"string".to_string(),
|
||||
span,
|
||||
r#"the strings "true" and "false" can be converted into a bool"#.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ impl Command for Into {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Apply into function."
|
||||
"Commands to convert data from one type to another."
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
@ -101,7 +101,7 @@ impl Command for SubCommand {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"converts text into datetime"
|
||||
"Convert text into a datetime"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -260,10 +260,11 @@ fn action(
|
||||
Ok(d) => Value::Date { val: d, span: head },
|
||||
Err(reason) => {
|
||||
return Value::Error {
|
||||
error: ShellError::CantConvert(
|
||||
error: ShellError::CantConvertWithHelp(
|
||||
format!("could not parse as datetime using format '{}'", dt.0),
|
||||
reason.to_string(),
|
||||
head,
|
||||
"you can use `into datetime` without a format string to enable flexible parsing".to_string()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ impl Command for SubCommand {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"converts text into decimal"
|
||||
"Convert text into a decimal"
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
@ -151,10 +151,11 @@ fn string_to_duration(s: &str, span: Span) -> Result<i64, ShellError> {
|
||||
}
|
||||
}
|
||||
|
||||
Err(ShellError::CantConvert(
|
||||
Err(ShellError::CantConvertWithHelp(
|
||||
"duration".to_string(),
|
||||
"string".to_string(),
|
||||
span,
|
||||
"supported units are ns, us, ms, sec, min, hr, day, and wk".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -206,8 +206,8 @@ fn convert_int(input: &Value, head: Span, radix: u32) -> Value {
|
||||
};
|
||||
match i64::from_str_radix(&i, radix) {
|
||||
Ok(n) => Value::Int { val: n, span: head },
|
||||
Err(reason) => Value::Error {
|
||||
error: ShellError::CantConvert("".to_string(), reason.to_string(), head),
|
||||
Err(_reason) => Value::Error {
|
||||
error: ShellError::CantConvert("int".to_string(), "string".to_string(), head),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -218,11 +218,12 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
|
||||
b if b.starts_with("0b") => {
|
||||
let num = match i64::from_str_radix(b.trim_start_matches("0b"), 2) {
|
||||
Ok(n) => n,
|
||||
Err(reason) => {
|
||||
return Err(ShellError::CantConvert(
|
||||
"could not parse as integer".to_string(),
|
||||
reason.to_string(),
|
||||
Err(_reason) => {
|
||||
return Err(ShellError::CantConvertWithHelp(
|
||||
"int".to_string(),
|
||||
"string".to_string(),
|
||||
span,
|
||||
r#"digits following "0b" can only be 0 or 1"#.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
@ -231,11 +232,13 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
|
||||
h if h.starts_with("0x") => {
|
||||
let num = match i64::from_str_radix(h.trim_start_matches("0x"), 16) {
|
||||
Ok(n) => n,
|
||||
Err(reason) => {
|
||||
return Err(ShellError::CantConvert(
|
||||
"could not parse as int".to_string(),
|
||||
reason.to_string(),
|
||||
Err(_reason) => {
|
||||
return Err(ShellError::CantConvertWithHelp(
|
||||
"int".to_string(),
|
||||
"string".to_string(),
|
||||
span,
|
||||
r#"hexadecimal digits following "0x" should be in 0-9, a-f, or A-F"#
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
@ -246,7 +249,7 @@ fn int_from_string(a_string: &str, span: Span) -> Result<i64, ShellError> {
|
||||
Err(_) => match a_string.parse::<f64>() {
|
||||
Ok(f) => Ok(f as i64),
|
||||
_ => Err(ShellError::CantConvert(
|
||||
"into int".to_string(),
|
||||
"int".to_string(),
|
||||
"string".to_string(),
|
||||
span,
|
||||
)),
|
||||
|
@ -257,6 +257,14 @@ pub fn action(
|
||||
span,
|
||||
),
|
||||
},
|
||||
Value::Binary { .. } => Value::Error {
|
||||
error: ShellError::CantConvertWithHelp(
|
||||
"string".into(),
|
||||
"binary".into(),
|
||||
span,
|
||||
"try using the `decode` command".into(),
|
||||
),
|
||||
},
|
||||
x => Value::Error {
|
||||
error: ShellError::CantConvert(String::from("string"), x.get_type().to_string(), span),
|
||||
},
|
||||
|
@ -25,6 +25,15 @@ impl Command for Alias {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -26,6 +26,15 @@ impl Command for Def {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -26,6 +26,15 @@ impl Command for DefEnv {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -17,7 +17,6 @@ impl Command for Do {
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("do")
|
||||
.desc(self.usage())
|
||||
.required("block", SyntaxShape::Any, "the block to run")
|
||||
.switch(
|
||||
"ignore-errors",
|
||||
|
@ -21,6 +21,15 @@ impl Command for ExportCommand {
|
||||
"Export custom commands or environment variables from a module."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
|
@ -15,7 +15,7 @@ impl Command for ExportAlias {
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("export def")
|
||||
Signature::build("export alias")
|
||||
.required("name", SyntaxShape::String, "name of the alias")
|
||||
.required(
|
||||
"initial_value",
|
||||
@ -25,6 +25,15 @@ impl Command for ExportAlias {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -26,6 +26,15 @@ impl Command for ExportDef {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -26,6 +26,15 @@ impl Command for ExportDefEnv {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -29,6 +29,15 @@ impl Command for ExportEnv {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -21,6 +21,15 @@ impl Command for ExportExtern {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -21,6 +21,15 @@ impl Command for Extern {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -44,6 +44,15 @@ impl Command for For {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -52,11 +61,15 @@ impl Command for For {
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let head = call.head;
|
||||
let var_id = call.positional[0]
|
||||
let var_id = call
|
||||
.positional_nth(0)
|
||||
.expect("checked through parser")
|
||||
.as_var()
|
||||
.expect("internal error: missing variable");
|
||||
|
||||
let keyword_expr = call.positional[1]
|
||||
let keyword_expr = call
|
||||
.positional_nth(1)
|
||||
.expect("checked through parser")
|
||||
.as_keyword()
|
||||
.expect("internal error: missing keyword");
|
||||
let values = eval_expression(engine_state, stack, keyword_expr)?;
|
||||
@ -116,7 +129,7 @@ impl Command for For {
|
||||
.filter(|x| !x.is_nothing())
|
||||
.into_pipeline_data(ctrlc)),
|
||||
Value::Range { val, .. } => Ok(val
|
||||
.into_range_iter()?
|
||||
.into_range_iter(ctrlc.clone())?
|
||||
.enumerate()
|
||||
.map(move |(idx, x)| {
|
||||
stack.with_env(&orig_env_vars, &orig_env_hidden);
|
||||
|
@ -7,6 +7,8 @@ use nu_protocol::{
|
||||
|
||||
use nu_engine::{get_full_help, CallExt};
|
||||
|
||||
use std::borrow::Borrow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Help;
|
||||
|
||||
@ -25,7 +27,7 @@ impl Command for Help {
|
||||
.named(
|
||||
"find",
|
||||
SyntaxShape::String,
|
||||
"string to find in command usage",
|
||||
"string to find in command names, usage, and search terms",
|
||||
Some('f'),
|
||||
)
|
||||
.category(Category::Core)
|
||||
@ -68,7 +70,7 @@ impl Command for Help {
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "search for string in command usage",
|
||||
description: "search for string in command names, usage and search terms",
|
||||
example: "help --find char",
|
||||
result: None,
|
||||
},
|
||||
@ -85,22 +87,33 @@ fn help(
|
||||
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
|
||||
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
|
||||
|
||||
let full_commands = engine_state.get_signatures_with_examples(false);
|
||||
let commands = engine_state.get_decl_ids_sorted(false);
|
||||
|
||||
if let Some(f) = find {
|
||||
let search_string = f.item.to_lowercase();
|
||||
let mut found_cmds_vec = Vec::new();
|
||||
|
||||
for (sig, _, is_plugin, is_custom) in full_commands {
|
||||
for decl_id in commands {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
let key = sig.name.clone();
|
||||
let c = sig.usage.clone();
|
||||
let e = sig.extra_usage.clone();
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
let matches_term = if !search_terms.is_empty() {
|
||||
search_terms
|
||||
.iter()
|
||||
.any(|term| term.to_lowercase().contains(&search_string))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if key.to_lowercase().contains(&search_string)
|
||||
|| c.to_lowercase().contains(&search_string)
|
||||
|| e.to_lowercase().contains(&search_string)
|
||||
|| usage.to_lowercase().contains(&search_string)
|
||||
|| matches_term
|
||||
{
|
||||
cols.push("name".into());
|
||||
vals.push(Value::String {
|
||||
@ -116,21 +129,37 @@ fn help(
|
||||
|
||||
cols.push("is_plugin".into());
|
||||
vals.push(Value::Bool {
|
||||
val: is_plugin,
|
||||
val: decl.is_plugin().is_some(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("is_custom".into());
|
||||
vals.push(Value::Bool {
|
||||
val: is_custom,
|
||||
val: decl.get_block_id().is_some(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("is_keyword".into());
|
||||
vals.push(Value::Bool {
|
||||
val: decl.is_parser_keyword(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("usage".into());
|
||||
vals.push(Value::String { val: c, span: head });
|
||||
vals.push(Value::String {
|
||||
val: usage,
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("extra_usage".into());
|
||||
vals.push(Value::String { val: e, span: head });
|
||||
cols.push("search_terms".into());
|
||||
vals.push(if search_terms.is_empty() {
|
||||
Value::nothing(head)
|
||||
} else {
|
||||
Value::String {
|
||||
val: search_terms.join(", "),
|
||||
span: head,
|
||||
}
|
||||
});
|
||||
|
||||
found_cmds_vec.push(Value::Record {
|
||||
cols,
|
||||
@ -149,13 +178,16 @@ fn help(
|
||||
let mut found_cmds_vec = Vec::new();
|
||||
|
||||
if rest[0].item == "commands" {
|
||||
for (sig, _, is_plugin, is_custom) in full_commands {
|
||||
for decl_id in commands {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
let key = sig.name.clone();
|
||||
let c = sig.usage.clone();
|
||||
let e = sig.extra_usage.clone();
|
||||
let decl = engine_state.get_decl(decl_id);
|
||||
let sig = decl.signature().update_from_command(decl.borrow());
|
||||
|
||||
let key = sig.name;
|
||||
let usage = sig.usage;
|
||||
let search_terms = sig.search_terms;
|
||||
|
||||
cols.push("name".into());
|
||||
vals.push(Value::String {
|
||||
@ -171,21 +203,37 @@ fn help(
|
||||
|
||||
cols.push("is_plugin".into());
|
||||
vals.push(Value::Bool {
|
||||
val: is_plugin,
|
||||
val: decl.is_plugin().is_some(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("is_custom".into());
|
||||
vals.push(Value::Bool {
|
||||
val: is_custom,
|
||||
val: decl.get_block_id().is_some(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("is_keyword".into());
|
||||
vals.push(Value::Bool {
|
||||
val: decl.is_parser_keyword(),
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("usage".into());
|
||||
vals.push(Value::String { val: c, span: head });
|
||||
vals.push(Value::String {
|
||||
val: usage,
|
||||
span: head,
|
||||
});
|
||||
|
||||
cols.push("extra_usage".into());
|
||||
vals.push(Value::String { val: e, span: head });
|
||||
cols.push("search_terms".into());
|
||||
vals.push(if search_terms.is_empty() {
|
||||
Value::nothing(head)
|
||||
} else {
|
||||
Value::String {
|
||||
val: search_terms.join(", "),
|
||||
span: head,
|
||||
}
|
||||
});
|
||||
|
||||
found_cmds_vec.push(Value::Record {
|
||||
cols,
|
||||
@ -207,7 +255,8 @@ fn help(
|
||||
name.push_str(&r.item);
|
||||
}
|
||||
|
||||
let output = full_commands
|
||||
let output = engine_state
|
||||
.get_signatures_with_examples(false)
|
||||
.iter()
|
||||
.filter(|(signature, _, _, _)| signature.name == name)
|
||||
.map(|(signature, examples, _, _)| {
|
||||
|
@ -23,7 +23,15 @@ impl Command for Hide {
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
"Symbols are hidden by priority: First aliases, then custom commands, then environment variables."
|
||||
r#"Symbols are hidden by priority: First aliases, then custom commands, then environment variables.
|
||||
|
||||
This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages
|
||||
"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
@ -36,7 +44,7 @@ impl Command for Hide {
|
||||
let import_pattern = if let Some(Expression {
|
||||
expr: Expr::ImportPattern(pat),
|
||||
..
|
||||
}) = call.positional.get(0)
|
||||
}) = call.positional_nth(0)
|
||||
{
|
||||
pat
|
||||
} else {
|
||||
@ -107,7 +115,11 @@ impl Command for Hide {
|
||||
};
|
||||
|
||||
if stack.remove_env_var(engine_state, &name).is_none() {
|
||||
return Err(ShellError::NotFound(call.positional[0].span));
|
||||
return Err(ShellError::NotFound(
|
||||
call.positional_nth(0)
|
||||
.expect("already checked for present positional")
|
||||
.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else if !import_pattern.hidden.contains(&import_pattern.head.name)
|
||||
|
@ -34,6 +34,15 @@ impl Command for If {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -41,9 +50,9 @@ impl Command for If {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let cond = &call.positional[0];
|
||||
let cond = call.positional_nth(0).expect("checked through parser");
|
||||
let then_block: CaptureBlock = call.req(engine_state, stack, 1)?;
|
||||
let else_case = call.positional.get(2);
|
||||
let else_case = call.positional_nth(2);
|
||||
|
||||
let result = eval_expression(engine_state, stack, cond)?;
|
||||
match &result {
|
||||
|
@ -26,6 +26,15 @@ impl Command for Let {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -33,11 +42,15 @@ impl Command for Let {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let var_id = call.positional[0]
|
||||
let var_id = call
|
||||
.positional_nth(0)
|
||||
.expect("checked through parser")
|
||||
.as_var()
|
||||
.expect("internal error: missing variable");
|
||||
|
||||
let keyword_expr = call.positional[1]
|
||||
let keyword_expr = call
|
||||
.positional_nth(1)
|
||||
.expect("checked through parser")
|
||||
.as_keyword()
|
||||
.expect("internal error: missing keyword");
|
||||
|
||||
|
@ -35,7 +35,7 @@ impl Command for Metadata {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let arg = call.positional.get(0);
|
||||
let arg = call.positional_nth(0);
|
||||
let head = call.head;
|
||||
|
||||
match arg {
|
||||
|
@ -25,6 +25,15 @@ impl Command for Module {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
|
@ -41,6 +41,15 @@ impl Command for Register {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
@ -55,12 +64,12 @@ impl Command for Register {
|
||||
vec![
|
||||
Example {
|
||||
description: "Register `nu_plugin_query` plugin from ~/.cargo/bin/ dir",
|
||||
example: r#"register -e capnp ~/.cargo/bin/nu_plugin_query"#,
|
||||
example: r#"register -e json ~/.cargo/bin/nu_plugin_query"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Register `nu_plugin_query` plugin from `nu -c`(plugin will be available in that nu session only)",
|
||||
example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register -e capnp ($plugin); version'"#,
|
||||
example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register -e json ($plugin); version'"#,
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
|
@ -26,6 +26,15 @@ impl Command for Source {
|
||||
"Runs a script file in the current context."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
|
@ -35,6 +35,10 @@ impl Command for Tutor {
|
||||
"Run the tutorial. To begin, run: tutor"
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["help", "learn", "tutorial"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -351,7 +355,7 @@ ls | each {|x| $x.name}
|
||||
```
|
||||
The above will create a list of the filenames in the directory.
|
||||
```
|
||||
if true { echo "it's true" } { echo "it's not true" }
|
||||
if true { echo "it's true" } else { echo "it's not true" }
|
||||
```
|
||||
This `if` call will run the first block if the expression is true, or the
|
||||
second block if the expression is false.
|
||||
|
@ -23,6 +23,15 @@ impl Command for Use {
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check
|
||||
https://www.nushell.sh/book/thinking_in_nushell.html#parsing-and-evaluation-are-different-stages"#
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
@ -33,7 +42,7 @@ impl Command for Use {
|
||||
let import_pattern = if let Some(Expression {
|
||||
expr: Expr::ImportPattern(pat),
|
||||
..
|
||||
}) = call.positional.get(0)
|
||||
}) = call.positional_nth(0)
|
||||
{
|
||||
pat
|
||||
} else {
|
||||
@ -72,7 +81,7 @@ impl Command for Use {
|
||||
for (name, span) in names {
|
||||
if let Some(id) = overlay.get_env_var_id(name) {
|
||||
output.push((name.clone(), id));
|
||||
} else if !overlay.has_decl(name) {
|
||||
} else if !overlay.has_decl(name) && !overlay.has_alias(name) {
|
||||
return Err(ShellError::EnvVarNotFoundAtRuntime(
|
||||
String::from_utf8_lossy(name).into(),
|
||||
*span,
|
||||
|
@ -1,4 +1,3 @@
|
||||
use indexmap::IndexMap;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{Example, IntoPipelineData, PipelineData, ShellError, Signature, Value};
|
||||
@ -49,144 +48,130 @@ pub fn version(
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let tag = call.head;
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
let mut indexmap = IndexMap::with_capacity(4);
|
||||
cols.push("version".to_string());
|
||||
vals.push(Value::String {
|
||||
val: env!("CARGO_PKG_VERSION").to_string(),
|
||||
span: tag,
|
||||
});
|
||||
|
||||
indexmap.insert(
|
||||
"version".to_string(),
|
||||
Value::String {
|
||||
val: env!("CARGO_PKG_VERSION").to_string(),
|
||||
span: tag,
|
||||
},
|
||||
);
|
||||
cols.push("branch".to_string());
|
||||
vals.push(Value::String {
|
||||
val: shadow_rs::branch(),
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
let branch: Option<&str> = Some(shadow::BRANCH).filter(|x| !x.is_empty());
|
||||
if let Some(branch) = branch {
|
||||
indexmap.insert(
|
||||
"branch".to_string(),
|
||||
Value::String {
|
||||
val: branch.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
}
|
||||
cols.push("tag".to_string());
|
||||
vals.push(Value::String {
|
||||
val: shadow_rs::tag(),
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
let short_commit: Option<&str> = Some(shadow::SHORT_COMMIT).filter(|x| !x.is_empty());
|
||||
if let Some(short_commit) = short_commit {
|
||||
indexmap.insert(
|
||||
"short_commit".to_string(),
|
||||
Value::String {
|
||||
val: short_commit.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("short_commit".to_string());
|
||||
vals.push(Value::String {
|
||||
val: short_commit.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
let commit_hash: Option<&str> = Some(shadow::COMMIT_HASH).filter(|x| !x.is_empty());
|
||||
if let Some(commit_hash) = commit_hash {
|
||||
indexmap.insert(
|
||||
"commit_hash".to_string(),
|
||||
Value::String {
|
||||
val: commit_hash.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("commit_hash".to_string());
|
||||
vals.push(Value::String {
|
||||
val: commit_hash.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
let commit_date: Option<&str> = Some(shadow::COMMIT_DATE).filter(|x| !x.is_empty());
|
||||
if let Some(commit_date) = commit_date {
|
||||
indexmap.insert(
|
||||
"commit_date".to_string(),
|
||||
Value::String {
|
||||
val: commit_date.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("commit_date".to_string());
|
||||
vals.push(Value::String {
|
||||
val: commit_date.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let build_os: Option<&str> = Some(shadow::BUILD_OS).filter(|x| !x.is_empty());
|
||||
if let Some(build_os) = build_os {
|
||||
indexmap.insert(
|
||||
"build_os".to_string(),
|
||||
Value::String {
|
||||
val: build_os.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("build_os".to_string());
|
||||
vals.push(Value::String {
|
||||
val: build_os.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let build_target: Option<&str> = Some(shadow::BUILD_TARGET).filter(|x| !x.is_empty());
|
||||
if let Some(build_target) = build_target {
|
||||
cols.push("build_os".to_string());
|
||||
vals.push(Value::String {
|
||||
val: build_target.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let rust_version: Option<&str> = Some(shadow::RUST_VERSION).filter(|x| !x.is_empty());
|
||||
if let Some(rust_version) = rust_version {
|
||||
indexmap.insert(
|
||||
"rust_version".to_string(),
|
||||
Value::String {
|
||||
val: rust_version.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("rust_version".to_string());
|
||||
vals.push(Value::String {
|
||||
val: rust_version.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let rust_channel: Option<&str> = Some(shadow::RUST_CHANNEL).filter(|x| !x.is_empty());
|
||||
if let Some(rust_channel) = rust_channel {
|
||||
indexmap.insert(
|
||||
"rust_channel".to_string(),
|
||||
Value::String {
|
||||
val: rust_channel.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("rust_channel".to_string());
|
||||
vals.push(Value::String {
|
||||
val: rust_channel.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let cargo_version: Option<&str> = Some(shadow::CARGO_VERSION).filter(|x| !x.is_empty());
|
||||
if let Some(cargo_version) = cargo_version {
|
||||
indexmap.insert(
|
||||
"cargo_version".to_string(),
|
||||
Value::String {
|
||||
val: cargo_version.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("cargo_version".to_string());
|
||||
vals.push(Value::String {
|
||||
val: cargo_version.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let pkg_version: Option<&str> = Some(shadow::PKG_VERSION).filter(|x| !x.is_empty());
|
||||
if let Some(pkg_version) = pkg_version {
|
||||
indexmap.insert(
|
||||
"pkg_version".to_string(),
|
||||
Value::String {
|
||||
val: pkg_version.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("pkg_version".to_string());
|
||||
vals.push(Value::String {
|
||||
val: pkg_version.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let build_time: Option<&str> = Some(shadow::BUILD_TIME).filter(|x| !x.is_empty());
|
||||
if let Some(build_time) = build_time {
|
||||
indexmap.insert(
|
||||
"build_time".to_string(),
|
||||
Value::String {
|
||||
val: build_time.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("build_time".to_string());
|
||||
vals.push(Value::String {
|
||||
val: build_time.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
let build_rust_channel: Option<&str> =
|
||||
Some(shadow::BUILD_RUST_CHANNEL).filter(|x| !x.is_empty());
|
||||
if let Some(build_rust_channel) = build_rust_channel {
|
||||
indexmap.insert(
|
||||
"build_rust_channel".to_string(),
|
||||
Value::String {
|
||||
val: build_rust_channel.to_string(),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("build_rust_channel".to_string());
|
||||
vals.push(Value::String {
|
||||
val: build_rust_channel.to_string(),
|
||||
span: call.head,
|
||||
});
|
||||
}
|
||||
|
||||
indexmap.insert(
|
||||
"features".to_string(),
|
||||
Value::String {
|
||||
val: features_enabled().join(", "),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("features".to_string());
|
||||
vals.push(Value::String {
|
||||
val: features_enabled().join(", "),
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
// Get a list of command names and check for plugins
|
||||
let installed_plugins = engine_state
|
||||
@ -196,28 +181,12 @@ pub fn version(
|
||||
.map(|x| x.name())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
indexmap.insert(
|
||||
"installed_plugins".to_string(),
|
||||
Value::String {
|
||||
val: installed_plugins.join(", "),
|
||||
span: call.head,
|
||||
},
|
||||
);
|
||||
cols.push("installed_plugins".to_string());
|
||||
vals.push(Value::String {
|
||||
val: installed_plugins.join(", "),
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
let cols = indexmap.keys().cloned().collect::<Vec<_>>();
|
||||
let vals = indexmap.values().cloned().collect::<Vec<_>>();
|
||||
|
||||
// Ok(Value::List {
|
||||
// vals: vec![Value::Record {
|
||||
// cols,
|
||||
// vals,
|
||||
// span: call.head,
|
||||
// }],
|
||||
// span: call.head,
|
||||
// }
|
||||
// .into_pipeline_data())
|
||||
|
||||
// List looks better than table, imo
|
||||
Ok(Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
@ -256,7 +225,7 @@ fn features_enabled() -> Vec<String> {
|
||||
names.push("uuid".to_string());
|
||||
}
|
||||
|
||||
#[cfg(feature = "which")]
|
||||
#[cfg(feature = "which-support")]
|
||||
{
|
||||
names.push("which".to_string());
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ impl Command for RenameDF {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"rename a dataframe column"
|
||||
"Rename a dataframe column"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
|
@ -17,7 +17,11 @@ impl Command for AsDate {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
r#"Converts string to date. Format example:
|
||||
r#"Converts string to date."#
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"Format example:
|
||||
"%Y-%m-%d" => 2021-12-31
|
||||
"%d-%m-%Y" => 31-12-2021
|
||||
"%Y%m%d" => 2021319 (2021-03-19)"#
|
||||
@ -25,7 +29,7 @@ impl Command for AsDate {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required("format", SyntaxShape::String, "formating date string")
|
||||
.required("format", SyntaxShape::String, "formatting date string")
|
||||
.switch("not-exact", "the format string may be contained in the date (e.g. foo-2021-01-01-bar could match 2021-01-01)", Some('n'))
|
||||
.category(Category::Custom("dataframe".into()))
|
||||
}
|
||||
|
@ -18,7 +18,11 @@ impl Command for AsDateTime {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
r#"Converts string to datetime. Format example:
|
||||
r#"Converts string to datetime."#
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"Format example:
|
||||
"%y/%m/%d %H:%M:%S" => 21/12/31 12:54:98
|
||||
"%y-%m-%d %H:%M:%S" => 2021-12-31 24:58:01
|
||||
"%y/%m/%d %H:%M:%S" => 21/12/31 24:58:01
|
||||
@ -34,7 +38,7 @@ impl Command for AsDateTime {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required("format", SyntaxShape::String, "formating date time string")
|
||||
.required("format", SyntaxShape::String, "formatting date time string")
|
||||
.switch("not-exact", "the format string may be contained in the date (e.g. foo-2021-01-01-bar could match 2021-01-01)", Some('n'))
|
||||
.category(Category::Custom("dataframe".into()))
|
||||
}
|
||||
|
@ -366,7 +366,8 @@ pub(super) fn compute_series_single_value(
|
||||
rhs_span: right.span()?,
|
||||
}),
|
||||
},
|
||||
Operator::Contains => match &right {
|
||||
// TODO: update this to do a regex match instead of a simple contains?
|
||||
Operator::RegexMatch => match &right {
|
||||
Value::String { val, .. } => contains_series_pat(&lhs, val, lhs_span),
|
||||
_ => Err(ShellError::OperatorMismatch {
|
||||
op_span: operator.span,
|
||||
@ -376,6 +377,19 @@ pub(super) fn compute_series_single_value(
|
||||
rhs_span: right.span()?,
|
||||
}),
|
||||
},
|
||||
Operator::StartsWith => match &right {
|
||||
Value::String { val, .. } => {
|
||||
let starts_with_pattern = format!("^{}", val);
|
||||
contains_series_pat(&lhs, &starts_with_pattern, lhs_span)
|
||||
}
|
||||
_ => Err(ShellError::OperatorMismatch {
|
||||
op_span: operator.span,
|
||||
lhs_ty: left.get_type(),
|
||||
lhs_span: left.span()?,
|
||||
rhs_ty: right.get_type(),
|
||||
rhs_span: right.span()?,
|
||||
}),
|
||||
},
|
||||
_ => Err(ShellError::OperatorMismatch {
|
||||
op_span: operator.span,
|
||||
lhs_ty: left.get_type(),
|
||||
|
@ -18,7 +18,7 @@ impl Command for Date {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"date"
|
||||
"Date-related commands"
|
||||
}
|
||||
|
||||
fn run(
|
||||
|
@ -4,6 +4,7 @@ mod humanize;
|
||||
mod list_timezone;
|
||||
mod now;
|
||||
mod parser;
|
||||
mod to_record;
|
||||
mod to_table;
|
||||
mod to_timezone;
|
||||
mod utils;
|
||||
@ -14,6 +15,7 @@ pub use format::SubCommand as DateFormat;
|
||||
pub use humanize::SubCommand as DateHumanize;
|
||||
pub use list_timezone::SubCommand as DateListTimezones;
|
||||
pub use now::SubCommand as DateNow;
|
||||
pub use to_record::SubCommand as DateToRecord;
|
||||
pub use to_table::SubCommand as DateToTable;
|
||||
pub use to_timezone::SubCommand as DateToTimezone;
|
||||
pub(crate) use utils::parse_date_from_string;
|
||||
|
164
crates/nu-command/src/date/to_record.rs
Normal file
164
crates/nu-command/src/date/to_record.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use crate::date::utils::parse_date_from_string;
|
||||
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError::DatetimeParseError, Signature, Span, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubCommand;
|
||||
|
||||
impl Command for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"date to-record"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("date to-record").category(Category::Date)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Convert the date into a structured table."
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let head = call.head;
|
||||
input.map(move |value| helper(value, head), engine_state.ctrlc.clone())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Convert the current date into a structured table.",
|
||||
example: "date to-table",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Convert the current date into a structured table.",
|
||||
example: "date now | date to-record",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Convert a given date into a structured table.",
|
||||
example: " '2020-04-12 22:10:57 +0200' | date to-record",
|
||||
result: {
|
||||
let span = Span::test_data();
|
||||
let cols = vec![
|
||||
"year".into(),
|
||||
"month".into(),
|
||||
"day".into(),
|
||||
"hour".into(),
|
||||
"minute".into(),
|
||||
"second".into(),
|
||||
"timezone".into(),
|
||||
];
|
||||
let vals = vec![
|
||||
Value::Int { val: 2020, span },
|
||||
Value::Int { val: 4, span },
|
||||
Value::Int { val: 12, span },
|
||||
Value::Int { val: 22, span },
|
||||
Value::Int { val: 10, span },
|
||||
Value::Int { val: 57, span },
|
||||
Value::String {
|
||||
val: "+02:00".to_string(),
|
||||
span,
|
||||
},
|
||||
];
|
||||
Some(Value::Record { cols, vals, span })
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_date_into_table(date: Result<DateTime<FixedOffset>, Value>, head: Span) -> Value {
|
||||
let cols = vec![
|
||||
"year".into(),
|
||||
"month".into(),
|
||||
"day".into(),
|
||||
"hour".into(),
|
||||
"minute".into(),
|
||||
"second".into(),
|
||||
"timezone".into(),
|
||||
];
|
||||
match date {
|
||||
Ok(x) => {
|
||||
let vals = vec![
|
||||
Value::Int {
|
||||
val: x.year() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.month() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.day() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.hour() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.minute() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::Int {
|
||||
val: x.second() as i64,
|
||||
span: head,
|
||||
},
|
||||
Value::String {
|
||||
val: x.offset().to_string(),
|
||||
span: head,
|
||||
},
|
||||
];
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: head,
|
||||
}
|
||||
}
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
fn helper(val: Value, head: Span) -> Value {
|
||||
match val {
|
||||
Value::String {
|
||||
val,
|
||||
span: val_span,
|
||||
} => {
|
||||
let date = parse_date_from_string(&val, val_span);
|
||||
parse_date_into_table(date, head)
|
||||
}
|
||||
Value::Nothing { span: _ } => {
|
||||
let now = Local::now();
|
||||
let n = now.with_timezone(now.offset());
|
||||
parse_date_into_table(Ok(n), head)
|
||||
}
|
||||
Value::Date { val, span: _ } => parse_date_into_table(Ok(val), head),
|
||||
_ => Value::Error {
|
||||
error: DatetimeParseError(head),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ impl Command for SubCommand {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Print the date in a structured table."
|
||||
"Convert the date into a structured table."
|
||||
}
|
||||
|
||||
fn run(
|
||||
@ -36,17 +36,17 @@ impl Command for SubCommand {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Print the date in a structured table.",
|
||||
description: "Convert the date into a structured table.",
|
||||
example: "date to-table",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Print the date in a structured table.",
|
||||
description: "Convert the date into a structured table.",
|
||||
example: "date now | date to-table",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Print the date in a structured table.",
|
||||
description: "Convert a given date into a structured table.",
|
||||
example: " '2020-04-12 22:10:57 +0200' | date to-table",
|
||||
result: {
|
||||
let span = Span::test_data();
|
||||
|
@ -80,11 +80,11 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
Headers,
|
||||
Insert,
|
||||
SplitBy,
|
||||
Keep,
|
||||
Take,
|
||||
Merge,
|
||||
Move,
|
||||
KeepUntil,
|
||||
KeepWhile,
|
||||
TakeWhile,
|
||||
TakeUntil,
|
||||
Last,
|
||||
Length,
|
||||
Lines,
|
||||
@ -106,6 +106,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
Skip,
|
||||
SkipUntil,
|
||||
SkipWhile,
|
||||
Sort,
|
||||
SortBy,
|
||||
Transpose,
|
||||
Uniq,
|
||||
@ -142,7 +143,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
Sys,
|
||||
};
|
||||
|
||||
#[cfg(feature = "which")]
|
||||
#[cfg(feature = "which-support")]
|
||||
bind_command! { Which };
|
||||
|
||||
// Strings
|
||||
@ -165,7 +166,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
StrContains,
|
||||
StrDowncase,
|
||||
StrEndswith,
|
||||
StrFindReplace,
|
||||
StrReplace,
|
||||
StrIndexOf,
|
||||
StrKebabCase,
|
||||
StrLength,
|
||||
@ -192,6 +193,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
Rm,
|
||||
Save,
|
||||
Touch,
|
||||
Glob,
|
||||
};
|
||||
|
||||
// Platform
|
||||
@ -217,6 +219,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
DateHumanize,
|
||||
DateListTimezones,
|
||||
DateNow,
|
||||
DateToRecord,
|
||||
DateToTable,
|
||||
DateToTimezone,
|
||||
};
|
||||
@ -367,6 +370,10 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
|
||||
MatchDeprecated,
|
||||
NthDeprecated,
|
||||
UnaliasDeprecated,
|
||||
StrFindReplaceDeprecated,
|
||||
KeepDeprecated,
|
||||
KeepUntilDeprecated,
|
||||
KeepWhileDeprecated,
|
||||
};
|
||||
|
||||
#[cfg(feature = "dataframe")]
|
||||
|
36
crates/nu-command/src/deprecated/keep_.rs
Normal file
36
crates/nu-command/src/deprecated/keep_.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, PipelineData, Signature,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeepDeprecated;
|
||||
|
||||
impl Command for KeepDeprecated {
|
||||
fn name(&self) -> &str {
|
||||
"keep"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Deprecated command"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Err(nu_protocol::ShellError::DeprecatedCommand(
|
||||
self.name().to_string(),
|
||||
"take".to_string(),
|
||||
call.head,
|
||||
))
|
||||
}
|
||||
}
|
36
crates/nu-command/src/deprecated/keep_until.rs
Normal file
36
crates/nu-command/src/deprecated/keep_until.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, PipelineData, Signature,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeepUntilDeprecated;
|
||||
|
||||
impl Command for KeepUntilDeprecated {
|
||||
fn name(&self) -> &str {
|
||||
"keep until"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Deprecated command"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Err(nu_protocol::ShellError::DeprecatedCommand(
|
||||
self.name().to_string(),
|
||||
"take until".to_string(),
|
||||
call.head,
|
||||
))
|
||||
}
|
||||
}
|
36
crates/nu-command/src/deprecated/keep_while.rs
Normal file
36
crates/nu-command/src/deprecated/keep_while.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, PipelineData, Signature,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeepWhileDeprecated;
|
||||
|
||||
impl Command for KeepWhileDeprecated {
|
||||
fn name(&self) -> &str {
|
||||
"keep while"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Deprecated command"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Err(nu_protocol::ShellError::DeprecatedCommand(
|
||||
self.name().to_string(),
|
||||
"take while".to_string(),
|
||||
call.head,
|
||||
))
|
||||
}
|
||||
}
|
@ -1,16 +1,24 @@
|
||||
mod keep_;
|
||||
mod keep_until;
|
||||
mod keep_while;
|
||||
mod match_;
|
||||
mod nth;
|
||||
mod pivot;
|
||||
mod str_datetime;
|
||||
mod str_decimal;
|
||||
mod str_find_replace;
|
||||
mod str_int;
|
||||
mod unalias;
|
||||
|
||||
pub use keep_::KeepDeprecated;
|
||||
pub use keep_until::KeepUntilDeprecated;
|
||||
pub use keep_while::KeepWhileDeprecated;
|
||||
pub use match_::MatchDeprecated;
|
||||
pub use nth::NthDeprecated;
|
||||
pub use pivot::PivotDeprecated;
|
||||
pub use str_datetime::StrDatetimeDeprecated;
|
||||
pub use str_decimal::StrDecimalDeprecated;
|
||||
pub use str_find_replace::StrFindReplaceDeprecated;
|
||||
pub use str_int::StrIntDeprecated;
|
||||
pub use unalias::UnaliasDeprecated;
|
||||
|
||||
|
36
crates/nu-command/src/deprecated/str_find_replace.rs
Normal file
36
crates/nu-command/src/deprecated/str_find_replace.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, PipelineData, Signature,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StrFindReplaceDeprecated;
|
||||
|
||||
impl Command for StrFindReplaceDeprecated {
|
||||
fn name(&self) -> &str {
|
||||
"str find-replace"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name()).category(Category::Deprecated)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Deprecated command"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
Err(nu_protocol::ShellError::DeprecatedCommand(
|
||||
self.name().to_string(),
|
||||
"str replace".to_string(),
|
||||
call.head,
|
||||
))
|
||||
}
|
||||
}
|
4
crates/nu-command/src/env/let_env.rs
vendored
4
crates/nu-command/src/env/let_env.rs
vendored
@ -35,7 +35,9 @@ impl Command for LetEnv {
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let env_var = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let keyword_expr = call.positional[1]
|
||||
let keyword_expr = call
|
||||
.positional_nth(1)
|
||||
.expect("checked through parser")
|
||||
.as_keyword()
|
||||
.expect("internal error: missing keyword");
|
||||
|
||||
|
2
crates/nu-command/src/env/load_env.rs
vendored
2
crates/nu-command/src/env/load_env.rs
vendored
@ -76,7 +76,7 @@ impl Command for LoadEnv {
|
||||
Ok(PipelineData::new(call.head))
|
||||
}
|
||||
_ => Err(ShellError::UnsupportedInput(
|
||||
"Record not supported".into(),
|
||||
"'load-env' expects a single record".into(),
|
||||
span,
|
||||
)),
|
||||
},
|
||||
|
8
crates/nu-command/src/env/with_env.rs
vendored
8
crates/nu-command/src/env/with_env.rs
vendored
@ -99,7 +99,9 @@ fn with_env(
|
||||
return Err(ShellError::CantConvert(
|
||||
"string list or single row".into(),
|
||||
x.get_type().to_string(),
|
||||
call.positional[1].span,
|
||||
call.positional_nth(1)
|
||||
.expect("already checked through .req")
|
||||
.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -123,7 +125,9 @@ fn with_env(
|
||||
return Err(ShellError::CantConvert(
|
||||
"string list or single row".into(),
|
||||
x.get_type().to_string(),
|
||||
call.positional[1].span,
|
||||
call.positional_nth(1)
|
||||
.expect("already checked through .req")
|
||||
.span,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ use crate::To;
|
||||
#[cfg(test)]
|
||||
use super::{
|
||||
Ansi, Date, From, If, Into, Math, Path, Random, Split, SplitColumn, SplitRow, Str, StrCollect,
|
||||
StrFindReplace, StrLength, Url, Wrap,
|
||||
StrLength, StrReplace, Url, Wrap,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@ -31,7 +31,7 @@ pub fn test_examples(cmd: impl Command + 'static) {
|
||||
working_set.add_decl(Box::new(Str));
|
||||
working_set.add_decl(Box::new(StrCollect));
|
||||
working_set.add_decl(Box::new(StrLength));
|
||||
working_set.add_decl(Box::new(StrFindReplace));
|
||||
working_set.add_decl(Box::new(StrReplace));
|
||||
working_set.add_decl(Box::new(BuildString));
|
||||
working_set.add_decl(Box::new(From));
|
||||
working_set.add_decl(Box::new(If));
|
||||
|
@ -20,7 +20,6 @@ impl Command for ViewSource {
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("view-source")
|
||||
.desc(self.usage())
|
||||
.required("item", SyntaxShape::Any, "name or block to view")
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ impl Command for Cd {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||
let raw_path = call.nth(0);
|
||||
let raw_path = call.positional_nth(0);
|
||||
let path_val: Option<Value> = call.opt(engine_state, stack, 0)?;
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
|
128
crates/nu-command/src/filesystem/glob.rs
Normal file
128
crates/nu-command/src/filesystem/glob.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use nu_engine::env::current_dir;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Spanned,
|
||||
SyntaxShape, Value,
|
||||
};
|
||||
use wax::Glob as WaxGlob;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Glob;
|
||||
|
||||
impl Command for Glob {
|
||||
fn name(&self) -> &str {
|
||||
"glob"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("glob")
|
||||
.required("glob", SyntaxShape::String, "the glob expression")
|
||||
.named(
|
||||
"depth",
|
||||
SyntaxShape::Int,
|
||||
"directory depth to search",
|
||||
Some('d'),
|
||||
)
|
||||
.category(Category::FileSystem)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Creates a list of files and/or folders based on the glob pattern provided."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Search for *.rs files",
|
||||
example: "glob *.rs",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Search for *.rs and *.toml files recursively up to 2 folders deep",
|
||||
example: "glob **/*.{rs,toml} --depth 2",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Search for files and folders that begin with uppercase C and lowercase c",
|
||||
example: r#"glob "[Cc]*""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description:
|
||||
"Search for files and folders like abc or xyz substituting a character for ?",
|
||||
example: r#"glob "{a?c,x?z}""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "A case-insensitive search for files and folders that begin with c",
|
||||
example: r#"glob "(?i)c*""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Search for files for folders that do not begin with c, C, b, M, or s",
|
||||
example: r#"glob "[!cCbMs]*""#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Search for files or folders with 3 a's in a row in the name",
|
||||
example: "glob <a*:3>",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Search for files or folders with only a, b, c, or d in the file name between 1 and 10 times",
|
||||
example: "glob <[a-d]:1,10>",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"For more glob pattern help please refer to https://github.com/olson-sean-k/wax"#
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let span = call.head;
|
||||
let path = current_dir(engine_state, stack)?;
|
||||
let glob_pattern: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let depth = call.get_flag(engine_state, stack, "depth")?;
|
||||
|
||||
let folder_depth = if let Some(depth) = depth {
|
||||
depth
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
|
||||
let glob = match WaxGlob::new(&glob_pattern.item) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Err(ShellError::LabeledError(
|
||||
"error with glob pattern".to_string(),
|
||||
format!("{}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let glob_results: Vec<Value> = glob
|
||||
.walk(path, folder_depth)
|
||||
.flatten()
|
||||
.map(|entry| Value::String {
|
||||
val: entry.into_path().to_string_lossy().to_string(),
|
||||
span,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(glob_results
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use crate::DirBuilder;
|
||||
use crate::DirInfo;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Local};
|
||||
use nu_engine::env::current_dir;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::ast::Call;
|
||||
@ -27,6 +27,10 @@ impl Command for Ls {
|
||||
"List the files in a directory."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["dir"]
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("ls")
|
||||
// Using a string instead of a glob pattern shape so it won't auto-expand
|
||||
@ -462,6 +466,8 @@ pub(crate) fn dir_entry_dict(
|
||||
} else {
|
||||
vals.push(Value::nothing(span));
|
||||
}
|
||||
} else {
|
||||
vals.push(Value::nothing(span));
|
||||
}
|
||||
} else {
|
||||
vals.push(Value::nothing(span));
|
||||
@ -471,9 +477,9 @@ pub(crate) fn dir_entry_dict(
|
||||
if long {
|
||||
cols.push("created".to_string());
|
||||
if let Ok(c) = md.created() {
|
||||
let utc: DateTime<Utc> = c.into();
|
||||
let utc: DateTime<Local> = c.into();
|
||||
vals.push(Value::Date {
|
||||
val: utc.into(),
|
||||
val: utc.with_timezone(utc.offset()),
|
||||
span,
|
||||
});
|
||||
} else {
|
||||
@ -482,9 +488,9 @@ pub(crate) fn dir_entry_dict(
|
||||
|
||||
cols.push("accessed".to_string());
|
||||
if let Ok(a) = md.accessed() {
|
||||
let utc: DateTime<Utc> = a.into();
|
||||
let utc: DateTime<Local> = a.into();
|
||||
vals.push(Value::Date {
|
||||
val: utc.into(),
|
||||
val: utc.with_timezone(utc.offset()),
|
||||
span,
|
||||
});
|
||||
} else {
|
||||
@ -494,9 +500,9 @@ pub(crate) fn dir_entry_dict(
|
||||
|
||||
cols.push("modified".to_string());
|
||||
if let Ok(m) = md.modified() {
|
||||
let utc: DateTime<Utc> = m.into();
|
||||
let utc: DateTime<Local> = m.into();
|
||||
vals.push(Value::Date {
|
||||
val: utc.into(),
|
||||
val: utc.with_timezone(utc.offset()),
|
||||
span,
|
||||
});
|
||||
} else {
|
||||
|
@ -57,13 +57,18 @@ impl Command for Mkdir {
|
||||
}
|
||||
|
||||
for (i, dir) in directories.enumerate() {
|
||||
let span = call.positional[i].span;
|
||||
let span = call
|
||||
.positional_nth(i)
|
||||
.expect("already checked through directories")
|
||||
.span;
|
||||
let dir_res = std::fs::create_dir_all(&dir);
|
||||
|
||||
if let Err(reason) = dir_res {
|
||||
return Err(ShellError::CreateNotPossible(
|
||||
format!("failed to create directory: {}", reason),
|
||||
call.positional[i].span,
|
||||
call.positional_nth(i)
|
||||
.expect("already checked through directories")
|
||||
.span,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod cd;
|
||||
mod cp;
|
||||
mod glob;
|
||||
mod ls;
|
||||
mod mkdir;
|
||||
mod mv;
|
||||
@ -11,6 +12,7 @@ mod util;
|
||||
|
||||
pub use cd::Cd;
|
||||
pub use cp::Cp;
|
||||
pub use glob::Glob;
|
||||
pub use ls::Ls;
|
||||
pub use mkdir::Mkdir;
|
||||
pub use mv::Mv;
|
||||
|
@ -6,7 +6,8 @@ use nu_engine::CallExt;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape,
|
||||
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
|
||||
Spanned, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions {
|
||||
@ -40,6 +41,7 @@ impl Command for Mv {
|
||||
SyntaxShape::Filepath,
|
||||
"the location to move files/directories to",
|
||||
)
|
||||
.switch("quiet", "suppress output showing files moved", Some('q'))
|
||||
// .switch("interactive", "ask user to confirm action", Some('i'))
|
||||
// .switch("force", "suppress error when no file", Some('f'))
|
||||
.category(Category::FileSystem)
|
||||
@ -55,9 +57,12 @@ impl Command for Mv {
|
||||
// TODO: handle invalid directory or insufficient permissions when moving
|
||||
let spanned_source: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let spanned_destination: Spanned<String> = call.req(engine_state, stack, 1)?;
|
||||
let quiet = call.has_flag("quiet");
|
||||
// let interactive = call.has_flag("interactive");
|
||||
// let force = call.has_flag("force");
|
||||
|
||||
let ctrlc = engine_state.ctrlc.clone();
|
||||
|
||||
let path = current_dir(engine_state, stack)?;
|
||||
let source = path.join(spanned_source.item.as_str());
|
||||
let destination = path.join(spanned_destination.item.as_str());
|
||||
@ -116,20 +121,35 @@ impl Command for Mv {
|
||||
.collect();
|
||||
}
|
||||
|
||||
for entry in sources.into_iter().flatten() {
|
||||
move_file(
|
||||
Spanned {
|
||||
item: entry,
|
||||
span: spanned_source.span,
|
||||
},
|
||||
Spanned {
|
||||
item: destination.clone(),
|
||||
span: spanned_destination.span,
|
||||
},
|
||||
)?
|
||||
}
|
||||
|
||||
Ok(PipelineData::new(call.head))
|
||||
let span = call.head;
|
||||
Ok(sources
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(move |entry| {
|
||||
let result = move_file(
|
||||
Spanned {
|
||||
item: entry.clone(),
|
||||
span: spanned_source.span,
|
||||
},
|
||||
Spanned {
|
||||
item: destination.clone(),
|
||||
span: spanned_destination.span,
|
||||
},
|
||||
);
|
||||
if let Err(error) = result {
|
||||
Some(Value::Error { error })
|
||||
} else if quiet {
|
||||
None
|
||||
} else {
|
||||
let val = format!(
|
||||
"moved {:} to {:}",
|
||||
entry.to_string_lossy(),
|
||||
destination.to_string_lossy()
|
||||
);
|
||||
Some(Value::String { val, span })
|
||||
}
|
||||
})
|
||||
.into_pipeline_data(ctrlc))
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use nu_engine::{get_full_help, CallExt};
|
||||
use nu_engine::{eval_block, get_full_help, CallExt};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
@ -141,12 +141,15 @@ impl Command for Open {
|
||||
|
||||
if let Some(ext) = ext {
|
||||
match engine_state.find_decl(format!("from {}", ext).as_bytes()) {
|
||||
Some(converter_id) => engine_state.get_decl(converter_id).run(
|
||||
engine_state,
|
||||
stack,
|
||||
&Call::new(arg_span),
|
||||
output,
|
||||
),
|
||||
Some(converter_id) => {
|
||||
let decl = engine_state.get_decl(converter_id);
|
||||
if let Some(block_id) = decl.get_block_id() {
|
||||
let block = engine_state.get_block(block_id);
|
||||
eval_block(engine_state, stack, block, output, false, false)
|
||||
} else {
|
||||
decl.run(engine_state, stack, &Call::new(arg_span), output)
|
||||
}
|
||||
}
|
||||
None => Ok(output),
|
||||
}
|
||||
} else {
|
||||
|
@ -1,5 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "trash-support")]
|
||||
#[cfg(all(
|
||||
feature = "trash-support",
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
use std::io::ErrorKind;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::FileTypeExt;
|
||||
@ -35,7 +39,13 @@ impl Command for Rm {
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("rm")
|
||||
let sig = Signature::build("rm");
|
||||
#[cfg(all(
|
||||
feature = "trash-support",
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
let sig = sig
|
||||
.switch(
|
||||
"trash",
|
||||
"use the platform's recycle bin instead of permanently deleting",
|
||||
@ -45,8 +55,8 @@ impl Command for Rm {
|
||||
"permanent",
|
||||
"don't use recycle bin, delete permanently",
|
||||
Some('p'),
|
||||
)
|
||||
.switch("recursive", "delete subdirectories recursively", Some('r'))
|
||||
);
|
||||
sig.switch("recursive", "delete subdirectories recursively", Some('r'))
|
||||
.switch("force", "suppress error when no file", Some('f'))
|
||||
.switch("quiet", "suppress output showing files deleted", Some('q'))
|
||||
// .switch("interactive", "ask user to confirm action", Some('i'))
|
||||
@ -69,12 +79,18 @@ impl Command for Rm {
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
let mut examples = vec![
|
||||
Example {
|
||||
description: "Delete or move a file to the system trash (depending on 'rm_always_trash' config option)",
|
||||
example: "rm file.txt",
|
||||
result: None,
|
||||
},
|
||||
}];
|
||||
#[cfg(all(
|
||||
feature = "trash-support",
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
examples.append(&mut vec![
|
||||
Example {
|
||||
description: "Move a file to the system trash",
|
||||
example: "rm --trash file.txt",
|
||||
@ -85,12 +101,13 @@ impl Command for Rm {
|
||||
example: "rm --permanent file.txt",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Delete a file, and suppress errors if no file is found",
|
||||
example: "rm --force file.txt",
|
||||
result: None,
|
||||
}
|
||||
]
|
||||
]);
|
||||
examples.push(Example {
|
||||
description: "Delete a file, and suppress errors if no file is found",
|
||||
example: "rm --force file.txt",
|
||||
result: None,
|
||||
});
|
||||
examples
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +117,11 @@ fn rm(
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let trash = call.has_flag("trash");
|
||||
#[cfg(feature = "trash-support")]
|
||||
#[cfg(all(
|
||||
feature = "trash-support",
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
let permanent = call.has_flag("permanent");
|
||||
let recursive = call.has_flag("recursive");
|
||||
let force = call.has_flag("force");
|
||||
@ -116,20 +137,26 @@ fn rm(
|
||||
|
||||
let rm_always_trash = config.rm_always_trash;
|
||||
|
||||
#[cfg(not(feature = "trash-support"))]
|
||||
#[cfg(any(
|
||||
not(feature = "trash-support"),
|
||||
target_os = "android",
|
||||
target_os = "ios"
|
||||
))]
|
||||
{
|
||||
if rm_always_trash {
|
||||
return Err(ShellError::SpannedLabeledError(
|
||||
"Cannot execute `rm`; the current configuration specifies \
|
||||
`rm_always_trash = true`, but the current nu executable was not \
|
||||
built with feature `trash_support`."
|
||||
built with feature `trash_support` or trash is not supported on \
|
||||
your platform."
|
||||
.into(),
|
||||
"trash required to be true but not supported".into(),
|
||||
span,
|
||||
));
|
||||
} else if trash {
|
||||
return Err(ShellError::SpannedLabeledError(
|
||||
"Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled"
|
||||
"Cannot execute `rm` with option `--trash`; feature `trash-support` not \
|
||||
enabled or trash is not supported on your platform"
|
||||
.into(),
|
||||
"this option is only available if nu is built with the `trash-support` feature"
|
||||
.into(),
|
||||
@ -241,7 +268,11 @@ fn rm(
|
||||
|| is_empty()
|
||||
{
|
||||
let result;
|
||||
#[cfg(feature = "trash-support")]
|
||||
#[cfg(all(
|
||||
feature = "trash-support",
|
||||
not(target_os = "android"),
|
||||
not(target_os = "ios")
|
||||
))]
|
||||
{
|
||||
use std::io::Error;
|
||||
result = if trash || (rm_always_trash && !permanent) {
|
||||
@ -254,7 +285,11 @@ fn rm(
|
||||
std::fs::remove_dir_all(&f)
|
||||
};
|
||||
}
|
||||
#[cfg(not(feature = "trash-support"))]
|
||||
#[cfg(any(
|
||||
not(feature = "trash-support"),
|
||||
target_os = "android",
|
||||
target_os = "ios"
|
||||
))]
|
||||
{
|
||||
result = if metadata.is_file() || is_socket || is_fifo {
|
||||
std::fs::remove_file(&f)
|
||||
|
@ -4,7 +4,7 @@ use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Value,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@ -125,39 +125,71 @@ impl Command for Save {
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
match input.into_value(span) {
|
||||
Value::String { val, .. } => {
|
||||
if let Err(err) = file.write_all(val.as_bytes()) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
}
|
||||
match input {
|
||||
PipelineData::ExternalStream { stdout: None, .. } => Ok(PipelineData::new(span)),
|
||||
PipelineData::ExternalStream {
|
||||
stdout: Some(mut stream),
|
||||
..
|
||||
} => {
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
Ok(PipelineData::new(span))
|
||||
stream
|
||||
.try_for_each(move |result| {
|
||||
let buf = match result {
|
||||
Ok(v) => match v {
|
||||
Value::String { val, .. } => val.into_bytes(),
|
||||
Value::Binary { val, .. } => val,
|
||||
_ => {
|
||||
return Err(ShellError::UnsupportedInput(
|
||||
format!("{:?} not supported", v.get_type()),
|
||||
v.span()?,
|
||||
));
|
||||
}
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
if let Err(err) = writer.write(&buf) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map(|_| PipelineData::new(span))
|
||||
}
|
||||
Value::Binary { val, .. } => {
|
||||
if let Err(err) = file.write_all(&val) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
input => match input.into_value(span) {
|
||||
Value::String { val, .. } => {
|
||||
if let Err(err) = file.write_all(val.as_bytes()) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
}
|
||||
|
||||
Ok(PipelineData::new(span))
|
||||
}
|
||||
Value::Binary { val, .. } => {
|
||||
if let Err(err) = file.write_all(&val) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
}
|
||||
|
||||
Ok(PipelineData::new(span))
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let val = vals
|
||||
.into_iter()
|
||||
.map(|it| it.as_string())
|
||||
.collect::<Result<Vec<String>, ShellError>>()?
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
|
||||
if let Err(err) = file.write_all(val.as_bytes()) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
Ok(PipelineData::new(span))
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let val = vals
|
||||
.into_iter()
|
||||
.map(|it| it.as_string())
|
||||
.collect::<Result<Vec<String>, ShellError>>()?
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
|
||||
Ok(PipelineData::new(span))
|
||||
}
|
||||
v => Err(ShellError::UnsupportedInput(
|
||||
format!("{:?} not supported", v.get_type()),
|
||||
span,
|
||||
)),
|
||||
if let Err(err) = file.write_all(val.as_bytes()) {
|
||||
return Err(ShellError::IOError(err.to_string()));
|
||||
}
|
||||
|
||||
Ok(PipelineData::new(span))
|
||||
}
|
||||
v => Err(ShellError::UnsupportedInput(
|
||||
format!("{:?} not supported", v.get_type()),
|
||||
span,
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,20 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Datelike, Local};
|
||||
use filetime::FileTime;
|
||||
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape};
|
||||
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape};
|
||||
|
||||
use crate::parse_date_from_string;
|
||||
|
||||
enum AddYear {
|
||||
Full,
|
||||
FirstDigits,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Touch;
|
||||
@ -20,6 +31,39 @@ impl Command for Touch {
|
||||
SyntaxShape::Filepath,
|
||||
"the path of the file you want to create",
|
||||
)
|
||||
.named(
|
||||
"timestamp",
|
||||
SyntaxShape::String,
|
||||
"change the file or directory time to a timestamp. Format: [[CC]YY]MMDDhhmm[.ss]\n\n If neither YY or CC is given, the current year will be assumed. If YY is specified, but CC is not, CC will be derived as follows:\n \tIf YY is between [69, 99], CC is 19\n \tIf YY is between [00, 68], CC is 20\n Note: It is expected that in a future version of this standard the default century inferred from a 2-digit year will change",
|
||||
Some('t'),
|
||||
)
|
||||
.named(
|
||||
"date",
|
||||
SyntaxShape::String,
|
||||
"change the file or directory time to a date",
|
||||
Some('d'),
|
||||
)
|
||||
.named(
|
||||
"reference",
|
||||
SyntaxShape::String,
|
||||
"change the file or directory time to the time of the reference file/directory",
|
||||
Some('r'),
|
||||
)
|
||||
.switch(
|
||||
"modified",
|
||||
"change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
|
||||
Some('m'),
|
||||
)
|
||||
.switch(
|
||||
"access",
|
||||
"change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used",
|
||||
Some('a'),
|
||||
)
|
||||
.switch(
|
||||
"no-create",
|
||||
"do not create the file if it does not exist",
|
||||
Some('c'),
|
||||
)
|
||||
.rest("rest", SyntaxShape::Filepath, "additional files to create")
|
||||
.category(Category::FileSystem)
|
||||
}
|
||||
@ -35,18 +79,218 @@ impl Command for Touch {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let mut change_mtime: bool = call.has_flag("modified");
|
||||
let mut change_atime: bool = call.has_flag("access");
|
||||
let use_stamp: bool = call.has_flag("timestamp");
|
||||
let use_date: bool = call.has_flag("date");
|
||||
let use_reference: bool = call.has_flag("reference");
|
||||
let no_create: bool = call.has_flag("no-create");
|
||||
let target: String = call.req(engine_state, stack, 0)?;
|
||||
let rest: Vec<String> = call.rest(engine_state, stack, 1)?;
|
||||
|
||||
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
|
||||
match OpenOptions::new().write(true).create(true).open(&item) {
|
||||
Ok(_) => continue,
|
||||
Err(err) => {
|
||||
return Err(ShellError::CreateNotPossible(
|
||||
format!("Failed to create file: {}", err),
|
||||
call.positional[index].span,
|
||||
let mut date: Option<DateTime<Local>> = None;
|
||||
let mut ref_date_atime: Option<DateTime<Local>> = None;
|
||||
|
||||
// Change both times if none is specified
|
||||
if !change_mtime && !change_atime {
|
||||
change_mtime = true;
|
||||
change_atime = true;
|
||||
}
|
||||
|
||||
if change_mtime || change_atime {
|
||||
date = Some(Local::now());
|
||||
}
|
||||
|
||||
if use_stamp || use_date {
|
||||
let (val, span) = if use_stamp {
|
||||
let stamp: Option<Spanned<String>> =
|
||||
call.get_flag(engine_state, stack, "timestamp")?;
|
||||
let (stamp, span) = match stamp {
|
||||
Some(stamp) => (stamp.item, stamp.span),
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter(
|
||||
"timestamp".to_string(),
|
||||
call.head,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Checks for the seconds stamp and removes the '.' delimiter if any
|
||||
let (val, has_sec): (String, bool) = match stamp.split_once('.') {
|
||||
Some((dtime, sec)) => (format!("{}{}", dtime, sec), true),
|
||||
None => (stamp.to_string(), false),
|
||||
};
|
||||
|
||||
let size = val.len();
|
||||
|
||||
// Each stamp is a 2 digit number and the whole stamp must not be less than 4 or greater than 7 pairs
|
||||
if (size % 2 != 0 || !(8..=14).contains(&size)) || val.parse::<usize>().is_err() {
|
||||
return Err(ShellError::UnsupportedInput(
|
||||
"input has an invalid timestamp".to_string(),
|
||||
span,
|
||||
));
|
||||
}
|
||||
|
||||
let add_year: Option<AddYear> = if has_sec {
|
||||
match size {
|
||||
10 => Some(AddYear::Full),
|
||||
12 => Some(AddYear::FirstDigits),
|
||||
14 => None,
|
||||
_ => unreachable!(), // This should never happen as the check above should catch it
|
||||
}
|
||||
} else {
|
||||
match size {
|
||||
8 => Some(AddYear::Full),
|
||||
10 => Some(AddYear::FirstDigits),
|
||||
12 => None,
|
||||
_ => unreachable!(), // This should never happen as the check above should catch it
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(add_year) = add_year {
|
||||
let year = Local::now().year();
|
||||
match add_year {
|
||||
AddYear::Full => (format!("{}{}", year, val), span),
|
||||
AddYear::FirstDigits => {
|
||||
// Compliance with the Unix version of touch
|
||||
let yy = val[0..2]
|
||||
.parse::<u8>()
|
||||
.expect("should be a valid 2 digit number");
|
||||
let mut year = 20;
|
||||
if (69..=99).contains(&yy) {
|
||||
year = 19;
|
||||
}
|
||||
(format!("{}{}", year, val), span)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(val, span)
|
||||
}
|
||||
} else {
|
||||
let date_string: Option<Spanned<String>> =
|
||||
call.get_flag(engine_state, stack, "date")?;
|
||||
match date_string {
|
||||
Some(date_string) => (date_string.item, date_string.span),
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter("date".to_string(), call.head));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
date = if let Ok(parsed_date) = parse_date_from_string(&val, span) {
|
||||
Some(parsed_date.into())
|
||||
} else {
|
||||
let flag = if use_stamp { "timestamp" } else { "date" };
|
||||
return Err(ShellError::UnsupportedInput(
|
||||
format!("input has an invalid {}", flag),
|
||||
span,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
if use_reference {
|
||||
let reference: Option<Spanned<String>> =
|
||||
call.get_flag(engine_state, stack, "reference")?;
|
||||
match reference {
|
||||
Some(reference) => {
|
||||
let reference_path = Path::new(&reference.item);
|
||||
if !reference_path.exists() {
|
||||
return Err(ShellError::UnsupportedInput(
|
||||
"path provided is invalid".to_string(),
|
||||
reference.span,
|
||||
));
|
||||
}
|
||||
|
||||
date = Some(
|
||||
reference_path
|
||||
.metadata()
|
||||
.expect("should be a valid path") // Should never fail as the path exists
|
||||
.modified()
|
||||
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
|
||||
.into(),
|
||||
);
|
||||
|
||||
ref_date_atime = Some(
|
||||
reference_path
|
||||
.metadata()
|
||||
.expect("should be a valid path") // Should never fail as the path exists
|
||||
.accessed()
|
||||
.expect("should have metadata") // This should always be valid as it is available on all nushell's supported platforms (Linux, Windows, MacOS)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter(
|
||||
"reference".to_string(),
|
||||
call.head,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, item) in vec![target].into_iter().chain(rest).enumerate() {
|
||||
if no_create {
|
||||
let path = Path::new(&item);
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = OpenOptions::new().write(true).create(true).open(&item) {
|
||||
return Err(ShellError::CreateNotPossible(
|
||||
format!("Failed to create file: {}", err),
|
||||
call.positional_nth(index)
|
||||
.expect("already checked positional")
|
||||
.span,
|
||||
));
|
||||
};
|
||||
|
||||
if change_mtime {
|
||||
// Should not panic as we return an error above if we can't parse the date
|
||||
if let Err(err) = filetime::set_file_mtime(
|
||||
&item,
|
||||
FileTime::from_system_time(date.expect("should be a valid date").into()),
|
||||
) {
|
||||
return Err(ShellError::ChangeModifiedTimeNotPossible(
|
||||
format!("Failed to change the modified time: {}", err),
|
||||
call.positional_nth(index)
|
||||
.expect("already checked positional")
|
||||
.span,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
if change_atime {
|
||||
// Reference file/directory may have different access and modified times
|
||||
if use_reference {
|
||||
// Should not panic as we return an error above if we can't parse the date
|
||||
if let Err(err) = filetime::set_file_atime(
|
||||
&item,
|
||||
FileTime::from_system_time(
|
||||
ref_date_atime.expect("should be a valid date").into(),
|
||||
),
|
||||
) {
|
||||
return Err(ShellError::ChangeAccessTimeNotPossible(
|
||||
format!("Failed to change the access time: {}", err),
|
||||
call.positional_nth(index)
|
||||
.expect("already checked positional")
|
||||
.span,
|
||||
));
|
||||
};
|
||||
} else {
|
||||
// Should not panic as we return an error above if we can't parse the date
|
||||
if let Err(err) = filetime::set_file_atime(
|
||||
&item,
|
||||
FileTime::from_system_time(date.expect("should be a valid date").into()),
|
||||
) {
|
||||
return Err(ShellError::ChangeAccessTimeNotPossible(
|
||||
format!("Failed to change the access time: {}", err),
|
||||
call.positional_nth(index)
|
||||
.expect("already checked positional")
|
||||
.span,
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +309,36 @@ impl Command for Touch {
|
||||
example: "touch a b c",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: r#"Changes the last modified time of "fixture.json" to today's date"#,
|
||||
example: "touch -m fixture.json",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Creates files d and e and set its last modified time to a timestamp",
|
||||
example: "touch -m -t 201908241230.30 d e",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Changes the last modified time of files a, b and c to a date",
|
||||
example: r#"touch -m -d "yesterday" a b c"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: r#"Changes the last modified time of file d and e to "fixture.json"'s last modified time"#,
|
||||
example: r#"touch -m -r fixture.json d e"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: r#"Changes the last accessed time of "fixture.json" to a date"#,
|
||||
example: r#"touch -a -d "August 24, 2019; 12:30:30" fixture.json"#,
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Changes both last modified and accessed time of a, b and c to a timestamp only if they exist",
|
||||
example: r#"touch -c -t 201908241230.30 a b c"#,
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -16,12 +16,16 @@ impl Command for Append {
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("append")
|
||||
.required("row", SyntaxShape::Any, "the row to append")
|
||||
.required("row", SyntaxShape::Any, "the row, list, or table to append")
|
||||
.category(Category::Filters)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Append a row to the table."
|
||||
"Append any number of rows to a table."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["add", "concatenate"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -81,12 +85,14 @@ impl Command for Append {
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let val: Value = call.req(engine_state, stack, 0)?;
|
||||
let vec: Vec<Value> = process_value(val);
|
||||
let metadata = input.metadata();
|
||||
|
||||
Ok(input
|
||||
.into_iter()
|
||||
.chain(vec)
|
||||
.into_iter()
|
||||
.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
.into_pipeline_data(engine_state.ctrlc.clone())
|
||||
.set_metadata(metadata))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
use nu_engine::{eval_block, CallExt};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{CaptureBlock, Command, EngineState, Stack};
|
||||
use nu_protocol::{Category, Example, PipelineData, Signature, SyntaxShape, Value};
|
||||
use nu_protocol::{
|
||||
Category, Example, IntoPipelineData, PipelineData, Signature, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Collect;
|
||||
@ -37,11 +39,12 @@ impl Command for Collect {
|
||||
let block = engine_state.get_block(capture_block.block_id).clone();
|
||||
let mut stack = stack.captures_to_stack(&capture_block.captures);
|
||||
|
||||
let metadata = input.metadata();
|
||||
let input: Value = input.into_value(call.head);
|
||||
|
||||
if let Some(var) = block.signature.get_positional(0) {
|
||||
if let Some(var_id) = &var.var_id {
|
||||
stack.add_var(*var_id, input);
|
||||
stack.add_var(*var_id, input.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,10 +52,11 @@ impl Command for Collect {
|
||||
engine_state,
|
||||
&mut stack,
|
||||
&block,
|
||||
PipelineData::new(call.head),
|
||||
input.into_pipeline_data(),
|
||||
call.redirect_stdout,
|
||||
call.redirect_stderr,
|
||||
)
|
||||
.map(|x| x.set_metadata(metadata))
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
|
@ -77,31 +77,34 @@ pub fn compact(
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, ShellError> {
|
||||
let columns: Vec<String> = call.rest(engine_state, stack, 0)?;
|
||||
input.filter(
|
||||
move |item| {
|
||||
match item {
|
||||
// Nothing is filtered out
|
||||
Value::Nothing { .. } => false,
|
||||
Value::Record { .. } => {
|
||||
for column in columns.iter() {
|
||||
match item.get_data_by_key(column) {
|
||||
None => return false,
|
||||
Some(x) => {
|
||||
if let Value::Nothing { .. } = x {
|
||||
return false;
|
||||
let metadata = input.metadata();
|
||||
input
|
||||
.filter(
|
||||
move |item| {
|
||||
match item {
|
||||
// Nothing is filtered out
|
||||
Value::Nothing { .. } => false,
|
||||
Value::Record { .. } => {
|
||||
for column in columns.iter() {
|
||||
match item.get_data_by_key(column) {
|
||||
None => return false,
|
||||
Some(x) => {
|
||||
if let Value::Nothing { .. } = x {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No defined columns contained Nothing
|
||||
true
|
||||
}
|
||||
// No defined columns contained Nothing
|
||||
true
|
||||
// Any non-Nothing, non-record should be kept
|
||||
_ => true,
|
||||
}
|
||||
// Any non-Nothing, non-record should be kept
|
||||
_ => true,
|
||||
}
|
||||
},
|
||||
engine_state.ctrlc.clone(),
|
||||
)
|
||||
},
|
||||
engine_state.ctrlc.clone(),
|
||||
)
|
||||
.map(|m| m.set_metadata(metadata))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user