engine-q merge

This commit is contained in:
Fernando Herrera
2022-02-07 19:11:34 +00:00
1965 changed files with 119062 additions and 20 deletions

View File

@ -1,4 +1,5 @@
[package]
<<<<<<< HEAD
authors = ["The Nu Project Contributors"]
description = "CLI for nushell"
edition = "2018"
@ -41,3 +42,24 @@ shadow-rs = "0.8.1"
default = ["shadow-rs"]
rustyline-support = ["rustyline", "nu-engine/rustyline-support"]
stable = []
=======
name = "nu-cli"
version = "0.1.0"
edition = "2021"
[dependencies]
nu-engine = { path = "../nu-engine" }
nu-path = { path = "../nu-path" }
nu-parser = { path = "../nu-parser" }
nu-protocol = { path = "../nu-protocol" }
# nu-ansi-term = { path = "../nu-ansi-term" }
nu-ansi-term = "0.42.0"
nu-color-config = { path = "../nu-color-config" }
miette = { version = "3.0.0", features = ["fancy"] }
thiserror = "1.0.29"
reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
log = "0.4"
is_executable = "1.0.1"
>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce

View File

@ -1,4 +0,0 @@
# nu-cli
This crate provides the fundamental needs when creating the Nushell interactive REPL. In it, you'll find features for interacting with the line editor (the piece which writes the prompt and takes input from the user), keybindings, handlers for the commandline arguments passed to the REPL as it starts up, and more.

View File

@ -1,3 +0,0 @@
fn main() -> shadow_rs::SdResult<()> {
shadow_rs::new()
}

View File

@ -1,638 +0,0 @@
mod logger;
mod options;
mod options_parser;
pub mod stopwatch;
use self::stopwatch::Stopwatch;
use lazy_static::lazy_static;
use nu_command::{commands::NuSignature as Nu, utils::test_bins as binaries};
use nu_engine::{get_full_help, EvaluationContext};
use nu_errors::ShellError;
use nu_protocol::hir::{Call, Expression, SpannedExpression, Synthetic};
use nu_protocol::{Primitive, UntaggedValue};
use nu_source::{Span, Tag};
use nu_stream::InputStream;
pub use options::{CliOptions, NuScript, Options};
use options_parser::{NuParser, OptionsParser};
use std::sync::Mutex;
lazy_static! {
pub static ref STOPWATCH: Mutex<Stopwatch> = {
let mut sw = Stopwatch::default();
sw.start();
sw.stop();
Mutex::new(sw)
};
}
pub struct App {
parser: Box<dyn OptionsParser>,
pub options: Options,
}
impl App {
pub fn new(parser: Box<dyn OptionsParser>, options: Options) -> Self {
Self { parser, options }
}
pub fn run(args: &[String]) -> Result<(), ShellError> {
let nu = Box::new(NuParser::new());
let options = Options::default();
let ui = App::new(nu, options);
ui.main(args)
}
pub fn main(&self, argv: &[String]) -> Result<(), ShellError> {
if self.perf() {
// start the stopwatch running
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
let argv = quote_positionals(argv).join(" ");
if let Err(cause) = self.parse(&argv) {
self.parser
.context()
.host()
.lock()
.print_err(cause, &nu_source::Text::from(argv));
std::process::exit(1);
}
if self.help() {
let context = self.parser.context();
let stream = nu_stream::OutputStream::one(
UntaggedValue::string(get_full_help(&Nu, &context.scope))
.into_value(nu_source::Tag::unknown()),
);
consume(context, stream)?;
if self.perf() {
// stop the stopwatch since we're exiting
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
eprintln!(
"help {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed()
);
}
std::process::exit(0);
}
if self.version() {
let context = self.parser.context();
let stream = nu_command::commands::version(nu_engine::CommandArgs {
context: context.clone(),
call_info: nu_engine::UnevaluatedCallInfo {
args: Call::new(
Box::new(SpannedExpression::new(
Expression::Synthetic(Synthetic::String("version".to_string())),
Span::unknown(),
)),
Span::unknown(),
),
name_tag: Tag::unknown(),
},
input: InputStream::empty(),
})?;
let stream = {
let command = context
.get_command("pivot")
.expect("could not find version command");
context.run_command(
command,
Tag::unknown(),
Call::new(
Box::new(SpannedExpression::new(
Expression::Synthetic(Synthetic::String("pivot".to_string())),
Span::unknown(),
)),
Span::unknown(),
),
stream,
)?
};
consume(context, stream)?;
if self.perf() {
// stop the stopwatch since we're exiting
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
eprintln!(
"version {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed()
);
}
std::process::exit(0);
}
if let Some(bin) = self.testbin() {
match bin.as_deref() {
Ok("echo_env") => binaries::echo_env(),
Ok("cococo") => binaries::cococo(),
Ok("meow") => binaries::meow(),
Ok("iecho") => binaries::iecho(),
Ok("fail") => binaries::fail(),
Ok("nonu") => binaries::nonu(),
Ok("chop") => binaries::chop(),
Ok("repeater") => binaries::repeater(),
_ => unreachable!(),
}
return Ok(());
}
let mut opts = CliOptions::new();
opts.config = self.config().map(std::ffi::OsString::from);
opts.stdin = self.takes_stdin();
opts.save_history = self.save_history();
opts.perf = self.perf();
use logger::{configure, debug_filters, logger, trace_filters};
logger(|builder| {
configure(self, builder)?;
trace_filters(self, builder)?;
debug_filters(self, builder)?;
Ok(())
})?;
if self.perf() {
// start a new split
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start()
}
if let Some(commands) = self.commands() {
let commands = commands?;
let script = NuScript::code(&commands)?;
opts.scripts = vec![script];
let context = crate::create_default_context(false)?;
return crate::run_script_file(context, opts);
}
if self.perf() {
// start a new spit
eprintln!(
"commands using -c at launch: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
if let Some(scripts) = self.scripts() {
let mut source_files = vec![];
for script in scripts {
let script_name = script?;
let path = std::ffi::OsString::from(&script_name);
match NuScript::source_file(path.as_os_str()) {
Ok(file) => source_files.push(file),
Err(_) => {
eprintln!("File not found: {}", script_name);
return Ok(());
}
}
}
for file in source_files {
let mut opts = opts.clone();
opts.scripts = vec![file];
let context = crate::create_default_context(false)?;
crate::run_script_file(context, opts)?;
}
return Ok(());
}
if self.perf() {
// start a new split
eprintln!(
"script file(s) passed in: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
let context = crate::create_default_context(true)?;
if !self.skip_plugins() {
let _ = crate::register_plugins(&context);
}
if self.perf() {
// start a new split
eprintln!(
"plugins registered: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
}
#[cfg(feature = "rustyline-support")]
{
crate::cli(context, opts)?;
}
#[cfg(not(feature = "rustyline-support"))]
{
println!("Nushell needs the 'rustyline-support' feature for CLI support");
}
Ok(())
}
pub fn commands(&self) -> Option<Result<String, ShellError>> {
self.options.get("commands").map(|v| match v.value {
UntaggedValue::Error(err) => Err(err),
UntaggedValue::Primitive(Primitive::String(name)) => Ok(name),
_ => Err(ShellError::untagged_runtime_error("Unsupported option")),
})
}
pub fn perf(&self) -> bool {
self.options
.get("perf")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn help(&self) -> bool {
self.options
.get("help")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn version(&self) -> bool {
self.options
.get("version")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn scripts(&self) -> Option<Vec<Result<String, ShellError>>> {
self.options.get("args").map(|v| {
v.table_entries()
.map(|v| match &v.value {
UntaggedValue::Error(err) => Err(err.clone()),
UntaggedValue::Primitive(Primitive::FilePath(path)) => {
Ok(path.display().to_string())
}
UntaggedValue::Primitive(Primitive::String(name)) => Ok(name.clone()),
_ => Err(ShellError::untagged_runtime_error("Unsupported option")),
})
.collect()
})
}
pub fn takes_stdin(&self) -> bool {
self.options
.get("stdin")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn config(&self) -> Option<String> {
self.options
.get("config-file")
.map(|v| v.as_string().expect("not a string"))
}
pub fn develop(&self) -> Option<Vec<Result<String, ShellError>>> {
self.options.get("develop").map(|v| {
let mut values = vec![];
match v.value {
UntaggedValue::Error(err) => values.push(Err(err)),
UntaggedValue::Primitive(Primitive::String(filters)) => {
values.extend(filters.split(',').map(|filter| Ok(filter.to_string())));
}
_ => values.push(Err(ShellError::untagged_runtime_error(
"Unsupported option",
))),
};
values
})
}
pub fn debug(&self) -> Option<Vec<Result<String, ShellError>>> {
self.options.get("debug").map(|v| {
let mut values = vec![];
match v.value {
UntaggedValue::Error(err) => values.push(Err(err)),
UntaggedValue::Primitive(Primitive::String(filters)) => {
values.extend(filters.split(',').map(|filter| Ok(filter.to_string())));
}
_ => values.push(Err(ShellError::untagged_runtime_error(
"Unsupported option",
))),
};
values
})
}
pub fn loglevel(&self) -> Option<Result<String, ShellError>> {
self.options.get("loglevel").map(|v| match v.value {
UntaggedValue::Error(err) => Err(err),
UntaggedValue::Primitive(Primitive::String(name)) => Ok(name),
_ => Err(ShellError::untagged_runtime_error("Unsupported option")),
})
}
pub fn testbin(&self) -> Option<Result<String, ShellError>> {
self.options.get("testbin").map(|v| match v.value {
UntaggedValue::Error(err) => Err(err),
UntaggedValue::Primitive(Primitive::String(name)) => Ok(name),
_ => Err(ShellError::untagged_runtime_error("Unsupported option")),
})
}
pub fn skip_plugins(&self) -> bool {
self.options
.get("skip-plugins")
.map(|v| matches!(v.as_bool(), Ok(true)))
.unwrap_or(false)
}
pub fn save_history(&self) -> bool {
self.options
.get("no-history")
.map(|v| !matches!(v.as_bool(), Ok(true)))
.unwrap_or(true)
}
pub fn parse(&self, args: &str) -> Result<(), ShellError> {
self.parser.parse(args).map(|options| {
self.options.swap(&options);
})
}
}
fn quote_positionals(parameters: &[String]) -> Vec<String> {
parameters
.iter()
.cloned()
.map(|arg| {
if arg.contains(' ') {
format!("\"{}\"", arg)
} else {
arg
}
})
.collect::<Vec<_>>()
}
fn consume(context: &EvaluationContext, stream: InputStream) -> Result<(), ShellError> {
let autoview_cmd = context
.get_command("autoview")
.expect("could not find autoview command");
let stream = context.run_command(
autoview_cmd,
Tag::unknown(),
Call::new(
Box::new(SpannedExpression::new(
Expression::Synthetic(Synthetic::String("autoview".to_string())),
Span::unknown(),
)),
Span::unknown(),
),
stream,
)?;
for _ in stream {}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn cli_app() -> App {
let parser = Box::new(NuParser::new());
let options = Options::default();
App::new(parser, options)
}
#[test]
fn default_options() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu")?;
assert!(!ui.version());
assert!(!ui.help());
assert!(!ui.takes_stdin());
assert!(ui.save_history());
assert!(!ui.skip_plugins());
assert_eq!(ui.config(), None);
assert_eq!(ui.loglevel(), None);
assert_eq!(ui.debug(), None);
assert_eq!(ui.develop(), None);
assert_eq!(ui.testbin(), None);
assert_eq!(ui.commands(), None);
assert_eq!(ui.scripts(), None);
Ok(())
}
#[test]
fn reports_errors_on_unsupported_flags() -> Result<(), ShellError> {
let ui = cli_app();
assert!(ui.parse("nu --coonfig-file /path/to/config.toml").is_err());
assert!(ui.config().is_none());
Ok(())
}
#[test]
fn configures_debug_trace_level_with_filters() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --develop=cli,parser")?;
assert_eq!(ui.develop().unwrap()[0], Ok("cli".to_string()));
assert_eq!(ui.develop().unwrap()[1], Ok("parser".to_string()));
Ok(())
}
#[test]
fn configures_debug_level_with_filters() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --debug=cli,run")?;
assert_eq!(ui.debug().unwrap()[0], Ok("cli".to_string()));
assert_eq!(ui.debug().unwrap()[1], Ok("run".to_string()));
Ok(())
}
#[test]
fn can_use_loglevels() -> Result<(), ShellError> {
for level in ["error", "warn", "info", "debug", "trace"] {
let ui = cli_app();
let args = format!("nu --loglevel={}", level);
ui.parse(&args)?;
assert_eq!(ui.loglevel().unwrap(), Ok(level.to_string()));
}
let ui = cli_app();
ui.parse("nu --loglevel=nada")?;
assert_eq!(
ui.loglevel().unwrap(),
Err(ShellError::untagged_runtime_error("nada is not supported."))
);
Ok(())
}
#[test]
fn can_be_login() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu -l")?;
let ui = cli_app();
ui.parse("nu --login")?;
Ok(())
}
#[test]
fn can_be_passed_nu_scripts() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu code.nu bootstrap.nu")?;
assert_eq!(ui.scripts().unwrap()[0], Ok("code.nu".into()));
assert_eq!(ui.scripts().unwrap()[1], Ok("bootstrap.nu".into()));
Ok(())
}
#[test]
fn can_use_test_binaries() -> Result<(), ShellError> {
for binarie_name in [
"echo_env", "cococo", "iecho", "fail", "nonu", "chop", "repeater", "meow",
] {
let ui = cli_app();
let args = format!("nu --testbin={}", binarie_name);
ui.parse(&args)?;
assert_eq!(ui.testbin().unwrap(), Ok(binarie_name.to_string()));
}
let ui = cli_app();
ui.parse("nu --testbin=andres")?;
assert_eq!(
ui.testbin().unwrap(),
Err(ShellError::untagged_runtime_error(
"andres is not supported."
))
);
Ok(())
}
#[test]
fn has_version() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --version")?;
assert!(ui.version());
Ok(())
}
#[test]
fn has_help() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --help")?;
assert!(ui.help());
Ok(())
}
#[test]
fn can_take_stdin() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --stdin")?;
assert!(ui.takes_stdin());
Ok(())
}
#[test]
fn can_opt_to_avoid_saving_history() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --no-history")?;
assert!(!ui.save_history());
Ok(())
}
#[test]
fn can_opt_to_skip_plugins() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --skip-plugins")?;
assert!(ui.skip_plugins());
Ok(())
}
#[test]
fn understands_commands_need_to_be_run() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu -c \"ls | get name\"")?;
assert_eq!(ui.commands().unwrap(), Ok(String::from("ls | get name")));
let ui = cli_app();
ui.parse("nu -c \"echo 'hola'\"")?;
assert_eq!(ui.commands().unwrap(), Ok(String::from("echo 'hola'")));
Ok(())
}
#[test]
fn knows_custom_configurations() -> Result<(), ShellError> {
let ui = cli_app();
ui.parse("nu --config-file /path/to/config.toml")?;
assert_eq!(ui.config().unwrap(), String::from("/path/to/config.toml"));
Ok(())
}
}

View File

@ -1,52 +0,0 @@
use super::App;
use log::LevelFilter;
use nu_errors::ShellError;
use pretty_env_logger::env_logger::Builder;
pub fn logger(f: impl FnOnce(&mut Builder) -> Result<(), ShellError>) -> Result<(), ShellError> {
let mut builder = pretty_env_logger::formatted_builder();
f(&mut builder)?;
let _ = builder.try_init();
Ok(())
}
pub fn configure(app: &App, logger: &mut Builder) -> Result<(), ShellError> {
if let Some(level) = app.loglevel() {
let level = match level.as_deref() {
Ok("error") => LevelFilter::Error,
Ok("warn") => LevelFilter::Warn,
Ok("info") => LevelFilter::Info,
Ok("debug") => LevelFilter::Debug,
Ok("trace") => LevelFilter::Trace,
Ok(_) | Err(_) => LevelFilter::Warn,
};
logger.filter_module("nu", level);
};
if let Ok(s) = std::env::var("RUST_LOG") {
logger.parse_filters(&s);
}
Ok(())
}
pub fn trace_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> {
if let Some(filters) = app.develop() {
filters.into_iter().filter_map(Result::ok).for_each(|name| {
logger.filter_module(&name, LevelFilter::Trace);
})
}
Ok(())
}
pub fn debug_filters(app: &App, logger: &mut Builder) -> Result<(), ShellError> {
if let Some(filters) = app.debug() {
filters.into_iter().filter_map(Result::ok).for_each(|name| {
logger.filter_module(&name, LevelFilter::Debug);
})
}
Ok(())
}

View File

@ -1,102 +0,0 @@
use indexmap::IndexMap;
use nu_errors::ShellError;
use nu_protocol::{UntaggedValue, Value};
use std::cell::RefCell;
use std::ffi::{OsStr, OsString};
#[derive(Debug, Clone)]
pub struct CliOptions {
pub config: Option<OsString>,
pub stdin: bool,
pub scripts: Vec<NuScript>,
pub save_history: bool,
pub perf: bool,
}
impl Default for CliOptions {
fn default() -> Self {
Self::new()
}
}
impl CliOptions {
pub fn new() -> Self {
Self {
config: None,
stdin: false,
scripts: vec![],
save_history: true,
perf: false,
}
}
}
#[derive(Debug)]
pub struct Options {
inner: RefCell<IndexMap<String, Value>>,
}
impl Options {
pub fn default() -> Self {
Self {
inner: RefCell::new(IndexMap::default()),
}
}
pub fn get(&self, key: &str) -> Option<Value> {
self.inner.borrow().get(key).cloned()
}
pub fn put(&self, key: &str, value: Value) {
self.inner.borrow_mut().insert(key.into(), value);
}
pub fn shift(&self) {
if let Some(Value {
value: UntaggedValue::Table(ref mut args),
..
}) = self.inner.borrow_mut().get_mut("args")
{
args.remove(0);
}
}
pub fn swap(&self, other: &Options) {
self.inner.swap(&other.inner);
}
}
#[derive(Debug, Clone)]
pub struct NuScript {
pub filepath: Option<OsString>,
pub contents: String,
}
impl NuScript {
pub fn code(content: &str) -> Result<Self, ShellError> {
Ok(Self {
filepath: None,
contents: content.to_string(),
})
}
pub fn get_code(&self) -> &str {
&self.contents
}
pub fn source_file(path: &OsStr) -> Result<Self, ShellError> {
use std::fs::File;
use std::io::Read;
let path = path.to_os_string();
let mut file = File::open(&path)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(Self {
filepath: Some(path),
contents: buffer,
})
}
}

View File

@ -1,132 +0,0 @@
use super::Options;
use nu_command::commands::{loglevels, testbins, NuSignature as Nu};
use nu_command::commands::{Autoview, Pivot, Table, Version as NuVersion};
use nu_engine::{whole_stream_command, EvaluationContext};
use nu_errors::ShellError;
use nu_protocol::hir::{ClassifiedCommand, InternalCommand, NamedValue};
use nu_protocol::UntaggedValue;
use nu_source::Tag;
pub struct NuParser {
context: EvaluationContext,
}
pub trait OptionsParser {
fn parse(&self, input: &str) -> Result<Options, ShellError>;
fn context(&self) -> &EvaluationContext;
}
impl NuParser {
pub fn new() -> Self {
let context = EvaluationContext::basic();
context.add_commands(vec![
whole_stream_command(Nu {}),
whole_stream_command(NuVersion {}),
whole_stream_command(Autoview {}),
whole_stream_command(Pivot {}),
whole_stream_command(Table {}),
]);
Self { context }
}
}
impl OptionsParser for NuParser {
fn context(&self) -> &EvaluationContext {
&self.context
}
fn parse(&self, input: &str) -> Result<Options, ShellError> {
let options = Options::default();
let (lite_result, _err) = nu_parser::lex(input, 0, nu_parser::NewlineMode::Normal);
let (lite_result, _err) = nu_parser::parse_block(lite_result);
let (parsed, err) = nu_parser::classify_block(&lite_result, &self.context.scope);
if let Some(reason) = err {
return Err(reason.into());
}
match parsed.block[0].pipelines[0].list[0] {
ClassifiedCommand::Internal(InternalCommand { ref args, .. }) => {
if let Some(ref params) = args.named {
params.iter().for_each(|(k, v)| {
let value = match v {
NamedValue::AbsentSwitch => {
Some(UntaggedValue::from(false).into_untagged_value())
}
NamedValue::PresentSwitch(span) => {
Some(UntaggedValue::from(true).into_value(Tag::from(span)))
}
NamedValue::AbsentValue => None,
NamedValue::Value(span, exprs) => {
let value = nu_engine::evaluate_baseline_expr(exprs, &self.context)
.expect("value");
Some(value.value.into_value(Tag::from(span)))
}
};
let value = value.map(|v| match k.as_ref() {
"testbin" => {
if let Ok(name) = v.as_string() {
if testbins().iter().any(|n| name == *n) {
v
} else {
UntaggedValue::Error(ShellError::untagged_runtime_error(
format!("{} is not supported.", name),
))
.into_value(v.tag)
}
} else {
v
}
}
"loglevel" => {
if let Ok(name) = v.as_string() {
if loglevels().iter().any(|n| name == *n) {
v
} else {
UntaggedValue::Error(ShellError::untagged_runtime_error(
format!("{} is not supported.", name),
))
.into_value(v.tag)
}
} else {
v
}
}
_ => v,
});
if let Some(value) = value {
options.put(k, value);
}
});
}
let mut positional_args = vec![];
if let Some(positional) = &args.positional {
for pos in positional {
let result = nu_engine::evaluate_baseline_expr(pos, &self.context)?;
positional_args.push(result);
}
}
if !positional_args.is_empty() {
options.put(
"args",
UntaggedValue::Table(positional_args).into_untagged_value(),
);
}
}
ClassifiedCommand::Error(ref reason) => {
return Err(reason.clone().into());
}
_ => return Err(ShellError::untagged_runtime_error("unrecognized command")),
}
Ok(options)
}
}

View File

@ -1,118 +0,0 @@
#![allow(dead_code)]
use std::default::Default;
use std::fmt;
use std::time::{Duration, Instant};
#[derive(Clone, Copy)]
pub struct Stopwatch {
/// The time the stopwatch was started last, if ever.
start_time: Option<Instant>,
/// The time the stopwatch was split last, if ever.
split_time: Option<Instant>,
/// The time elapsed while the stopwatch was running (between start() and stop()).
elapsed: Duration,
}
impl Default for Stopwatch {
fn default() -> Stopwatch {
Stopwatch {
start_time: None,
split_time: None,
elapsed: Duration::from_secs(0),
}
}
}
impl fmt::Display for Stopwatch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}ms", self.elapsed_ms());
}
}
impl Stopwatch {
/// Returns a new stopwatch.
pub fn new() -> Stopwatch {
let sw: Stopwatch = Default::default();
sw
}
/// Returns a new stopwatch which will immediately be started.
pub fn start_new() -> Stopwatch {
let mut sw = Stopwatch::new();
sw.start();
sw
}
/// Starts the stopwatch.
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
}
/// Stops the stopwatch.
pub fn stop(&mut self) {
self.elapsed = self.elapsed();
self.start_time = None;
self.split_time = None;
}
/// Resets all counters and stops the stopwatch.
pub fn reset(&mut self) {
self.elapsed = Duration::from_secs(0);
self.start_time = None;
self.split_time = None;
}
/// Resets and starts the stopwatch again.
pub fn restart(&mut self) {
self.reset();
self.start();
}
/// Returns whether the stopwatch is running.
pub fn is_running(&self) -> bool {
self.start_time.is_some()
}
/// Returns the elapsed time since the start of the stopwatch.
pub fn elapsed(&self) -> Duration {
match self.start_time {
// stopwatch is running
Some(t1) => t1.elapsed() + self.elapsed,
// stopwatch is not running
None => self.elapsed,
}
}
/// Returns the elapsed time since the start of the stopwatch in milliseconds.
pub fn elapsed_ms(&self) -> i64 {
let dur = self.elapsed();
(dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64
}
/// Returns the elapsed time since last split or start/restart.
///
/// If the stopwatch is in stopped state this will always return a zero Duration.
pub fn elapsed_split(&mut self) -> Duration {
match self.start_time {
// stopwatch is running
Some(start) => {
let res = match self.split_time {
Some(split) => split.elapsed(),
None => start.elapsed(),
};
self.split_time = Some(Instant::now());
res
}
// stopwatch is not running
None => Duration::from_secs(0),
}
}
/// Returns the elapsed time since last split or start/restart in milliseconds.
///
/// If the stopwatch is in stopped state this will always return zero.
pub fn elapsed_split_ms(&mut self) -> i64 {
let dur = self.elapsed_split();
(dur.as_secs() * 1000 + dur.subsec_millis() as u64) as i64
}
}

View File

@ -1,538 +0,0 @@
use crate::app::STOPWATCH;
use crate::line_editor::configure_ctrl_c;
use nu_ansi_term::Color;
use nu_engine::{maybe_print_errors, run_block, script::run_script_standalone, EvaluationContext};
#[allow(unused_imports)]
pub(crate) use nu_engine::script::{process_script, LineResult};
#[cfg(feature = "rustyline-support")]
use crate::line_editor::{
configure_rustyline_editor, convert_rustyline_result_to_string,
default_rustyline_editor_configuration, nu_line_editor_helper,
};
#[allow(unused_imports)]
use nu_data::config;
use nu_source::{Tag, Text};
use nu_stream::InputStream;
#[allow(unused_imports)]
use std::sync::atomic::Ordering;
#[cfg(feature = "rustyline-support")]
use rustyline::{self, error::ReadlineError};
use nu_errors::ShellError;
use nu_parser::ParserScope;
use nu_path::expand_tilde;
use nu_protocol::{hir::ExternalRedirection, ConfigPath, UntaggedValue, Value};
use log::trace;
use std::error::Error;
use std::iter::Iterator;
use std::path::PathBuf;
// Name of environment variable where the prompt could be stored
#[cfg(feature = "rustyline-support")]
const PROMPT_COMMAND: &str = "PROMPT_COMMAND";
pub fn search_paths() -> Vec<std::path::PathBuf> {
use std::env;
let mut search_paths = Vec::new();
// Automatically add path `nu` is in as a search path
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
search_paths.push(exe_dir.to_path_buf());
}
}
if let Ok(config) = nu_data::config::config(Tag::unknown()) {
if let Some(Value {
value: UntaggedValue::Table(pipelines),
..
}) = config.get("plugin_dirs")
{
for pipeline in pipelines {
if let Ok(plugin_dir) = pipeline.as_string() {
search_paths.push(expand_tilde(plugin_dir));
}
}
}
}
search_paths
}
pub fn run_script_file(
context: EvaluationContext,
options: super::app::CliOptions,
) -> Result<(), ShellError> {
if let Some(cfg) = options.config {
load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
} else {
load_global_cfg(&context);
}
let _ = register_plugins(&context);
let _ = configure_ctrl_c(&context);
let script = options
.scripts
.get(0)
.ok_or_else(|| ShellError::unexpected("Nu source code not available"))?;
run_script_standalone(script.get_code().to_string(), options.stdin, &context, true)?;
Ok(())
}
#[cfg(feature = "rustyline-support")]
fn default_prompt_string(cwd: &str) -> String {
format!(
"{}{}{}{}{}{}> ",
Color::Green.bold().prefix(),
cwd,
nu_ansi_term::ansi::RESET,
Color::Cyan.bold().prefix(),
current_branch(),
nu_ansi_term::ansi::RESET
)
}
#[cfg(feature = "rustyline-support")]
fn evaluate_prompt_string(prompt_line: &str, context: &EvaluationContext, cwd: &str) -> String {
context.scope.enter_scope();
let (prompt_block, err) = nu_parser::parse(prompt_line, 0, &context.scope);
if err.is_some() {
context.scope.exit_scope();
default_prompt_string(cwd)
} else {
let run_result = run_block(
&prompt_block,
context,
InputStream::empty(),
ExternalRedirection::Stdout,
);
context.scope.exit_scope();
match run_result {
Ok(result) => match result.collect_string(Tag::unknown()) {
Ok(string_result) => {
let errors = context.get_errors();
maybe_print_errors(context, Text::from(prompt_line));
context.clear_errors();
if !errors.is_empty() {
"> ".into()
} else {
string_result.item
}
}
Err(e) => {
context.host().lock().print_err(e, &Text::from(prompt_line));
context.clear_errors();
"> ".into()
}
},
Err(e) => {
context.host().lock().print_err(e, &Text::from(prompt_line));
context.clear_errors();
"> ".into()
}
}
}
}
#[cfg(feature = "rustyline-support")]
pub fn cli(
context: EvaluationContext,
options: super::app::CliOptions,
) -> Result<(), Box<dyn Error>> {
let _ = configure_ctrl_c(&context);
// start time for running startup scripts (this metric includes loading of the cfg, but w/e)
let startup_commands_start_time = std::time::Instant::now();
if let Some(cfg) = options.config {
load_cfg_as_global_cfg(&context, PathBuf::from(cfg));
} else {
load_global_cfg(&context);
}
// Store cmd duration in an env var
context.scope.add_env_var(
"CMD_DURATION_MS",
startup_commands_start_time
.elapsed()
.as_millis()
.to_string(),
);
if options.perf {
eprintln!(
"config loaded: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//Configure rustyline
let mut rl = default_rustyline_editor_configuration();
let history_path = if let Some(cfg) = &context.configs().lock().global_config {
let _ = configure_rustyline_editor(&mut rl, cfg);
let helper = Some(nu_line_editor_helper(&context, cfg));
rl.set_helper(helper);
nu_data::config::path::history_path_or_default(cfg)
} else {
nu_data::config::path::default_history_path()
};
if options.perf {
eprintln!(
"rustyline configuration: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
// Don't load history if it's not necessary
if options.save_history {
let _ = rl.load_history(&history_path);
}
if options.perf {
eprintln!(
"history load: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//set vars from cfg if present
let (skip_welcome_message, prompt) = if let Some(cfg) = &context.configs().lock().global_config
{
(
cfg.var("skip_welcome_message")
.map(|x| x.is_true())
.unwrap_or(false),
cfg.var("prompt"),
)
} else {
(false, None)
};
if options.perf {
eprintln!(
"load custom prompt: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.start();
}
//Check whether dir we start in contains local cfg file and if so load it.
load_local_cfg_if_present(&context);
// Give ourselves a scope to work in
context.scope.enter_scope();
let mut session_text = String::new();
let mut line_start: usize = 0;
if !skip_welcome_message {
println!(
"Welcome to Nushell {} (type 'help' for more info)",
nu_command::commands::core_version()
);
}
#[cfg(windows)]
{
let _ = nu_ansi_term::enable_ansi_support();
}
let mut ctrlcbreak = false;
if options.perf {
eprintln!(
"timing stopped. starting run loop: {:?}",
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.elapsed_split()
);
STOPWATCH
.lock()
.expect("unable to lock the stopwatch")
.stop();
}
loop {
if context.ctrl_c().load(Ordering::SeqCst) {
context.ctrl_c().store(false, Ordering::SeqCst);
continue;
}
let cwd = context.shell_manager().path();
// Check if the PROMPT_COMMAND env variable is set. This env variable
// contains nu code that is used to overwrite the prompt
let colored_prompt = match context.scope.get_env(PROMPT_COMMAND) {
Some(env_prompt) => evaluate_prompt_string(&env_prompt, &context, &cwd),
None => {
if let Some(prompt) = &prompt {
let prompt_line = prompt.as_string()?;
evaluate_prompt_string(&prompt_line, &context, &cwd)
} else {
default_prompt_string(&cwd)
}
}
};
let prompt = {
if let Ok(bytes) = strip_ansi_escapes::strip(&colored_prompt) {
String::from_utf8_lossy(&bytes).to_string()
} else {
"> ".to_string()
}
};
if let Some(helper) = rl.helper_mut() {
helper.colored_prompt = colored_prompt;
}
let mut initial_command = Some(String::new());
let mut readline = Err(ReadlineError::Eof);
while let Some(ref cmd) = initial_command {
readline = rl.readline_with_initial(&prompt, (cmd, ""));
initial_command = None;
}
if let Ok(line) = &readline {
line_start = session_text.len();
session_text.push_str(line);
session_text.push('\n');
}
// start time for command duration
let cmd_start_time = std::time::Instant::now();
let line = match convert_rustyline_result_to_string(readline) {
LineResult::Success(_) => process_script(
&session_text[line_start..],
&context,
false,
line_start,
true,
),
x => x,
};
// Store cmd duration in an env var
context.scope.add_env_var(
"CMD_DURATION_MS",
cmd_start_time.elapsed().as_millis().to_string(),
);
match line {
LineResult::Success(line) => {
if options.save_history && !line.trim().is_empty() {
rl.add_history_entry(&line);
let _ = rl.append_history(&history_path);
}
maybe_print_errors(&context, Text::from(session_text.clone()));
}
LineResult::ClearHistory => {
if options.save_history {
rl.clear_history();
std::fs::remove_file(&history_path)?;
}
}
LineResult::Error(line, err) => {
if options.save_history && !line.trim().is_empty() {
rl.add_history_entry(&line);
let _ = rl.append_history(&history_path);
}
context
.host()
.lock()
.print_err(err, &Text::from(session_text.clone()));
// I am not so sure, we don't need maybe_print_errors here (as we printed an err
// above), because maybe_print_errors also clears the errors.
// TODO Analyze where above err comes from, and whether we need to clear
// context.errors here
// Or just be consistent and return errors always in context.errors...
maybe_print_errors(&context, Text::from(session_text.clone()));
}
LineResult::CtrlC => {
let config_ctrlc_exit = context
.configs()
.lock()
.global_config
.as_ref()
.and_then(|cfg| cfg.var("ctrlc_exit"))
.map(|ctrl_c| ctrl_c.is_true())
.unwrap_or(false); // default behavior is to allow CTRL-C spamming similar to other shells
if !config_ctrlc_exit {
continue;
}
if ctrlcbreak {
if options.save_history {
let _ = rl.append_history(&history_path);
}
std::process::exit(0);
} else {
context.with_host(|host| host.stdout("CTRL-C pressed (again to quit)"));
ctrlcbreak = true;
continue;
}
}
LineResult::CtrlD => {
context.shell_manager().remove_at_current();
if context.shell_manager().is_empty() {
break;
}
}
LineResult::Break => {
break;
}
}
ctrlcbreak = false;
}
// we are ok if we can not save history
if options.save_history {
let _ = rl.append_history(&history_path);
}
Ok(())
}
#[cfg(feature = "rustyline-support")]
pub fn load_local_cfg_if_present(context: &EvaluationContext) {
trace!("Loading local cfg if present");
match config::loadable_cfg_exists_in_dir(PathBuf::from(context.shell_manager().path())) {
Ok(Some(cfg_path)) => {
if let Err(err) = context.load_config(&ConfigPath::Local(cfg_path)) {
context.host().lock().print_err(err, &Text::from(""))
}
}
Err(e) => {
//Report error while checking for local cfg file
context.host().lock().print_err(e, &Text::from(""))
}
Ok(None) => {
//No local cfg file present in start dir
}
}
}
fn load_cfg_as_global_cfg(context: &EvaluationContext, path: PathBuf) {
if let Err(err) = context.load_config(&ConfigPath::Global(path)) {
context.host().lock().print_err(err, &Text::from(""));
}
}
pub fn load_global_cfg(context: &EvaluationContext) {
match config::default_path() {
Ok(path) => {
load_cfg_as_global_cfg(context, path);
}
Err(e) => {
context.host().lock().print_err(e, &Text::from(""));
}
}
}
pub fn register_plugins(context: &EvaluationContext) -> Result<(), ShellError> {
if let Ok(plugins) = nu_engine::plugin::build_plugin::scan(search_paths()) {
context.add_commands(
plugins
.into_iter()
.filter(|p| !context.is_command_registered(p.name()))
.collect(),
);
}
Ok(())
}
pub fn parse_and_eval(line: &str, ctx: &EvaluationContext) -> Result<String, ShellError> {
// FIXME: do we still need this?
let line = if let Some(s) = line.strip_suffix('\n') {
s
} else {
line
};
// TODO ensure the command whose examples we're testing is actually in the pipeline
ctx.scope.enter_scope();
let (classified_block, err) = nu_parser::parse(line, 0, &ctx.scope);
if let Some(err) = err {
ctx.scope.exit_scope();
return Err(err.into());
}
let input_stream = InputStream::empty();
let result = run_block(
&classified_block,
ctx,
input_stream,
ExternalRedirection::Stdout,
);
ctx.scope.exit_scope();
result?.collect_string(Tag::unknown()).map(|x| x.item)
}
#[allow(dead_code)]
fn current_branch() -> String {
#[cfg(feature = "shadow-rs")]
{
Some(shadow_rs::branch())
.map(|x| x.trim().to_string())
.filter(|x| !x.is_empty())
.map(|x| format!("({})", x))
.unwrap_or_default()
}
#[cfg(not(feature = "shadow-rs"))]
{
"".to_string()
}
}

View File

@ -0,0 +1,442 @@
use nu_engine::eval_block;
use nu_parser::{flatten_expression, parse};
use nu_protocol::{
ast::{Expr, Statement},
engine::{EngineState, Stack, StateWorkingSet},
PipelineData, Span,
};
use reedline::Completer;
const SEP: char = std::path::MAIN_SEPARATOR;
#[derive(Clone)]
pub struct NuCompleter {
engine_state: EngineState,
}
impl NuCompleter {
pub fn new(engine_state: EngineState) -> Self {
Self { engine_state }
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
let mut executables = vec![];
let paths;
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", "$scope", "$in", "$config", "$env"];
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,
) -> 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 prefix = working_set.get_span_contents(span);
let prefix = String::from_utf8_lossy(prefix).to_string();
let results_external =
self.external_command_completion(&prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
x,
)
});
results
.into_iter()
.chain(results_external.into_iter())
.collect()
}
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 pos = offset + pos;
let (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
for stmt in output.stmts.into_iter() {
if let Statement::Pipeline(pipeline) = stmt {
for expr in pipeline.expressions {
let flattened = flatten_expression(&working_set, &expr);
for flat in flattened {
if pos >= flat.0.start && pos <= flat.0.end {
let prefix = working_set.get_span_contents(flat.0);
if prefix.starts_with(b"$") {
return self.complete_variables(
&working_set,
prefix,
flat.0,
offset,
);
}
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 {
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: flat.0.start - offset,
end: flat.0.end - offset,
},
String::from_utf8_lossy(&named).to_string(),
));
}
}
return output;
}
}
match &flat.1 {
nu_parser::FlatShape::Custom(custom_completion) => {
let prefix = working_set.get_span_contents(flat.0).to_vec();
let (block, ..) = parse(
&mut working_set,
None,
custom_completion.as_bytes(),
false,
);
let mut stack = Stack::default();
let result = eval_block(
&self.engine_state,
&mut stack,
&block,
PipelineData::new(flat.0),
);
let v: Vec<_> = match result {
Ok(pd) => pd
.into_iter()
.map(move |x| {
let s = x.as_string().expect(
"FIXME: better error handling for custom completions",
);
(
reedline::Span {
start: flat.0.start - offset,
end: flat.0.end - offset,
},
s,
)
})
.filter(|x| x.1.as_bytes().starts_with(&prefix))
.collect(),
_ => vec![],
};
return v;
}
nu_parser::FlatShape::External
| nu_parser::FlatShape::InternalCall
| nu_parser::FlatShape::String => {
let subcommands = self.complete_commands(
&working_set,
Span {
start: expr.span.start,
end: pos,
},
offset,
);
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 = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(prefix).to_string();
return file_path_completion(flat.0, &prefix, &cwd)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
x.1,
)
})
.chain(subcommands.into_iter())
.collect();
}
nu_parser::FlatShape::Filepath
| nu_parser::FlatShape::GlobPattern
| nu_parser::FlatShape::ExternalArg => {
// Check for subcommands
let subcommands = self.complete_commands(
&working_set,
Span {
start: expr.span.start,
end: pos,
},
offset,
);
// Check for args
let prefix = working_set.get_span_contents(flat.0);
let prefix = String::from_utf8_lossy(prefix).to_string();
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 results = file_path_completion(flat.0, &prefix, &cwd);
return results
.into_iter()
.map(move |x| {
(
reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
x.1,
)
})
.chain(subcommands.into_iter())
.collect();
}
_ => {
return self.complete_commands(
&working_set,
Span {
start: expr.span.start,
end: pos,
},
offset,
)
}
}
}
// If we get here, let's just check to see if we can complete a subcommand
// Check for subcommands
let subcommands = self.complete_commands(
&working_set,
Span {
start: expr.span.start,
end: pos,
},
offset,
);
if !subcommands.is_empty() {
return subcommands;
}
}
}
}
}
vec![]
}
}
impl Completer for NuCompleter {
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
let mut output = self.completion_helper(line, pos);
output.sort_by(|a, b| a.1.cmp(&b.1));
output
}
}
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 = if let Some(s) = partial.strip_prefix('"') {
s
} else {
partial
};
let partial = if let Some(s) = partial.strip_suffix('"') {
s
} else {
partial
};
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())
}

View File

@ -0,0 +1,46 @@
use miette::{LabeledSpan, MietteHandler, ReportHandler, Severity, SourceCode};
use nu_protocol::engine::StateWorkingSet;
use thiserror::Error;
/// This error exists so that we can defer SourceCode handling. It simply
/// forwards most methods, except for `.source_code()`, which we provide.
#[derive(Error)]
#[error("{0}")]
pub struct CliError<'src>(
pub &'src (dyn miette::Diagnostic + Send + Sync + 'static),
pub &'src StateWorkingSet<'src>,
);
impl std::fmt::Debug for CliError<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
MietteHandler::default().debug(self, f)?;
Ok(())
}
}
impl<'src> miette::Diagnostic for CliError<'src> {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.0.code()
}
fn severity(&self) -> Option<Severity> {
self.0.severity()
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.0.help()
}
fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.0.url()
}
fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
self.0.labels()
}
// Finally, we redirect the source_code method to our own source.
fn source_code(&self) -> Option<&dyn SourceCode> {
Some(&self.1)
}
}

View File

@ -1,473 +0,0 @@
use rustyline::{KeyCode as RustyKeyCode, Modifiers};
use serde::{Deserialize, Serialize};
pub fn convert_keyevent(key_event: KeyCode, modifiers: Option<Modifiers>) -> rustyline::KeyEvent {
match key_event {
KeyCode::UnknownEscSeq => convert_to_rl_keyevent(RustyKeyCode::UnknownEscSeq, modifiers),
KeyCode::Backspace => convert_to_rl_keyevent(RustyKeyCode::Backspace, modifiers),
KeyCode::BackTab => convert_to_rl_keyevent(RustyKeyCode::BackTab, modifiers),
KeyCode::BracketedPasteStart => {
convert_to_rl_keyevent(RustyKeyCode::BracketedPasteStart, modifiers)
}
KeyCode::BracketedPasteEnd => {
convert_to_rl_keyevent(RustyKeyCode::BracketedPasteEnd, modifiers)
}
KeyCode::Char(c) => convert_to_rl_keyevent(RustyKeyCode::Char(c), modifiers),
KeyCode::Delete => convert_to_rl_keyevent(RustyKeyCode::Delete, modifiers),
KeyCode::Down => convert_to_rl_keyevent(RustyKeyCode::Down, modifiers),
KeyCode::End => convert_to_rl_keyevent(RustyKeyCode::End, modifiers),
KeyCode::Enter => convert_to_rl_keyevent(RustyKeyCode::Enter, modifiers),
KeyCode::Esc => convert_to_rl_keyevent(RustyKeyCode::Esc, modifiers),
KeyCode::F(u) => convert_to_rl_keyevent(RustyKeyCode::F(u), modifiers),
KeyCode::Home => convert_to_rl_keyevent(RustyKeyCode::Home, modifiers),
KeyCode::Insert => convert_to_rl_keyevent(RustyKeyCode::Insert, modifiers),
KeyCode::Left => convert_to_rl_keyevent(RustyKeyCode::Left, modifiers),
KeyCode::Null => convert_to_rl_keyevent(RustyKeyCode::Null, modifiers),
KeyCode::PageDown => convert_to_rl_keyevent(RustyKeyCode::PageDown, modifiers),
KeyCode::PageUp => convert_to_rl_keyevent(RustyKeyCode::PageUp, modifiers),
KeyCode::Right => convert_to_rl_keyevent(RustyKeyCode::Right, modifiers),
KeyCode::Tab => convert_to_rl_keyevent(RustyKeyCode::Tab, modifiers),
KeyCode::Up => convert_to_rl_keyevent(RustyKeyCode::Up, modifiers),
}
}
fn convert_to_rl_keyevent(
key_code: RustyKeyCode,
modifier: Option<Modifiers>,
) -> rustyline::KeyEvent {
rustyline::KeyEvent {
0: key_code,
1: modifier.unwrap_or(Modifiers::NONE),
}
}
fn convert_word(word: Word) -> rustyline::Word {
match word {
Word::Big => rustyline::Word::Big,
Word::Emacs => rustyline::Word::Emacs,
Word::Vi => rustyline::Word::Vi,
}
}
fn convert_at(at: At) -> rustyline::At {
match at {
At::AfterEnd => rustyline::At::AfterEnd,
At::BeforeEnd => rustyline::At::BeforeEnd,
At::Start => rustyline::At::Start,
}
}
fn convert_char_search(search: CharSearch) -> rustyline::CharSearch {
match search {
CharSearch::Backward(c) => rustyline::CharSearch::Backward(c),
CharSearch::BackwardAfter(c) => rustyline::CharSearch::BackwardAfter(c),
CharSearch::Forward(c) => rustyline::CharSearch::Forward(c),
CharSearch::ForwardBefore(c) => rustyline::CharSearch::ForwardBefore(c),
}
}
fn convert_movement(movement: Movement) -> rustyline::Movement {
match movement {
Movement::BackwardChar(u) => rustyline::Movement::BackwardChar(u),
Movement::BackwardWord { repeat, word } => {
rustyline::Movement::BackwardWord(repeat, convert_word(word))
}
Movement::BeginningOfBuffer => rustyline::Movement::BeginningOfBuffer,
Movement::BeginningOfLine => rustyline::Movement::BeginningOfLine,
Movement::EndOfBuffer => rustyline::Movement::EndOfBuffer,
Movement::EndOfLine => rustyline::Movement::EndOfLine,
Movement::ForwardChar(u) => rustyline::Movement::ForwardChar(u),
Movement::ForwardWord { repeat, at, word } => {
rustyline::Movement::ForwardWord(repeat, convert_at(at), convert_word(word))
}
Movement::LineDown(u) => rustyline::Movement::LineDown(u),
Movement::LineUp(u) => rustyline::Movement::LineUp(u),
Movement::ViCharSearch { repeat, search } => {
rustyline::Movement::ViCharSearch(repeat, convert_char_search(search))
}
Movement::ViFirstPrint => rustyline::Movement::ViFirstPrint,
Movement::WholeBuffer => rustyline::Movement::WholeBuffer,
Movement::WholeLine => rustyline::Movement::WholeLine,
}
}
fn convert_anchor(anchor: Anchor) -> rustyline::Anchor {
match anchor {
Anchor::After => rustyline::Anchor::After,
Anchor::Before => rustyline::Anchor::Before,
}
}
fn convert_cmd(cmd: Cmd) -> rustyline::Cmd {
match cmd {
Cmd::Abort => rustyline::Cmd::Abort,
Cmd::AcceptLine => rustyline::Cmd::AcceptLine,
Cmd::AcceptOrInsertLine => rustyline::Cmd::AcceptOrInsertLine {
accept_in_the_middle: false,
},
Cmd::BeginningOfHistory => rustyline::Cmd::BeginningOfHistory,
Cmd::CapitalizeWord => rustyline::Cmd::CapitalizeWord,
Cmd::ClearScreen => rustyline::Cmd::ClearScreen,
Cmd::Complete => rustyline::Cmd::Complete,
Cmd::CompleteBackward => rustyline::Cmd::CompleteBackward,
Cmd::CompleteHint => rustyline::Cmd::CompleteHint,
Cmd::Dedent(movement) => rustyline::Cmd::Dedent(convert_movement(movement)),
Cmd::DowncaseWord => rustyline::Cmd::DowncaseWord,
Cmd::EndOfFile => rustyline::Cmd::EndOfFile,
Cmd::EndOfHistory => rustyline::Cmd::EndOfHistory,
Cmd::ForwardSearchHistory => rustyline::Cmd::ForwardSearchHistory,
Cmd::HistorySearchBackward => rustyline::Cmd::HistorySearchBackward,
Cmd::HistorySearchForward => rustyline::Cmd::HistorySearchForward,
Cmd::Indent(movement) => rustyline::Cmd::Indent(convert_movement(movement)),
Cmd::Insert { repeat, string } => rustyline::Cmd::Insert(repeat, string),
Cmd::Interrupt => rustyline::Cmd::Interrupt,
Cmd::Kill(movement) => rustyline::Cmd::Kill(convert_movement(movement)),
Cmd::LineDownOrNextHistory(u) => rustyline::Cmd::LineDownOrNextHistory(u),
Cmd::LineUpOrPreviousHistory(u) => rustyline::Cmd::LineUpOrPreviousHistory(u),
Cmd::Move(movement) => rustyline::Cmd::Move(convert_movement(movement)),
Cmd::NextHistory => rustyline::Cmd::NextHistory,
Cmd::Newline => rustyline::Cmd::Newline,
Cmd::Noop => rustyline::Cmd::Noop,
Cmd::Overwrite(c) => rustyline::Cmd::Overwrite(c),
#[cfg(windows)]
Cmd::PasteFromClipboard => rustyline::Cmd::PasteFromClipboard,
Cmd::PreviousHistory => rustyline::Cmd::PreviousHistory,
Cmd::QuotedInsert => rustyline::Cmd::QuotedInsert,
Cmd::Replace {
movement,
replacement,
} => rustyline::Cmd::Replace(convert_movement(movement), replacement),
Cmd::ReplaceChar { repeat, ch } => rustyline::Cmd::ReplaceChar(repeat, ch),
Cmd::ReverseSearchHistory => rustyline::Cmd::ReverseSearchHistory,
Cmd::SelfInsert { repeat, ch } => rustyline::Cmd::SelfInsert(repeat, ch),
Cmd::Suspend => rustyline::Cmd::Suspend,
Cmd::TransposeChars => rustyline::Cmd::TransposeChars,
Cmd::TransposeWords(u) => rustyline::Cmd::TransposeWords(u),
Cmd::Undo(u) => rustyline::Cmd::Undo(u),
Cmd::Unknown => rustyline::Cmd::Unknown,
Cmd::UpcaseWord => rustyline::Cmd::UpcaseWord,
Cmd::ViYankTo(movement) => rustyline::Cmd::ViYankTo(convert_movement(movement)),
Cmd::Yank { repeat, anchor } => rustyline::Cmd::Yank(repeat, convert_anchor(anchor)),
Cmd::YankPop => rustyline::Cmd::YankPop,
}
}
fn convert_keybinding(keybinding: Keybinding) -> (rustyline::KeyEvent, rustyline::Cmd) {
let rusty_modifiers = match keybinding.modifiers {
Some(mods) => match mods {
NuModifiers::Ctrl => Some(Modifiers::CTRL),
NuModifiers::Alt => Some(Modifiers::ALT),
NuModifiers::Shift => Some(Modifiers::SHIFT),
NuModifiers::None => Some(Modifiers::NONE),
NuModifiers::CtrlShift => Some(Modifiers::CTRL_SHIFT),
NuModifiers::AltShift => Some(Modifiers::ALT_SHIFT),
NuModifiers::CtrlAlt => Some(Modifiers::CTRL_ALT),
NuModifiers::CtrlAltShift => Some(Modifiers::CTRL_ALT_SHIFT),
// _ => None,
},
None => None,
};
(
convert_keyevent(keybinding.key, rusty_modifiers),
convert_cmd(keybinding.binding),
)
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum KeyCode {
/// Unsupported escape sequence (on unix platform)
UnknownEscSeq,
/// ⌫ or `KeyEvent::Ctrl('H')`
Backspace,
/// ⇤ (usually Shift-Tab)
BackTab,
/// Paste (on unix platform)
BracketedPasteStart,
/// Paste (on unix platform)
BracketedPasteEnd,
/// Single char
Char(char),
/// ⌦
Delete,
/// ↓ arrow key
Down,
/// ⇲
End,
/// ↵ or `KeyEvent::Ctrl('M')`
Enter,
/// Escape or `KeyEvent::Ctrl('[')`
Esc,
/// Function key
F(u8),
/// ⇱
Home,
/// Insert key
Insert,
/// ← arrow key
Left,
// /// `KeyEvent::Char('\0')`
Null,
/// ⇟
PageDown,
/// ⇞
PageUp,
/// → arrow key
Right,
/// ⇥ or `KeyEvent::Ctrl('I')`
Tab,
/// ↑ arrow key
Up,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Cmd {
/// abort
Abort, // Miscellaneous Command
/// accept-line
AcceptLine,
/// beginning-of-history
BeginningOfHistory,
/// capitalize-word
CapitalizeWord,
/// clear-screen
ClearScreen,
/// complete
Complete,
/// complete-backward
CompleteBackward,
/// complete-hint
CompleteHint,
/// Dedent current line
Dedent(Movement),
/// downcase-word
DowncaseWord,
/// vi-eof-maybe
EndOfFile,
/// end-of-history
EndOfHistory,
/// forward-search-history
ForwardSearchHistory,
/// history-search-backward
HistorySearchBackward,
/// history-search-forward
HistorySearchForward,
/// Indent current line
Indent(Movement),
/// Insert text
Insert { repeat: RepeatCount, string: String },
/// Interrupt signal (Ctrl-C)
Interrupt,
/// backward-delete-char, backward-kill-line, backward-kill-word
/// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout,
/// vi-delete, vi-delete-to, vi-rubout
Kill(Movement),
/// backward-char, backward-word, beginning-of-line, end-of-line,
/// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word,
/// vi-prev-word
Move(Movement),
/// Inserts a newline
Newline,
/// next-history
NextHistory,
/// No action
Noop,
/// vi-replace
Overwrite(char),
/// Paste from the clipboard
#[cfg(windows)]
PasteFromClipboard,
/// previous-history
PreviousHistory,
/// quoted-insert
QuotedInsert,
/// vi-change-char
ReplaceChar { repeat: RepeatCount, ch: char },
/// vi-change-to, vi-substitute
Replace {
movement: Movement,
replacement: Option<String>,
},
/// reverse-search-history
ReverseSearchHistory,
/// self-insert
SelfInsert { repeat: RepeatCount, ch: char },
/// Suspend signal (Ctrl-Z on unix platform)
Suspend,
/// transpose-chars
TransposeChars,
/// transpose-words
TransposeWords(RepeatCount),
/// undo
Undo(RepeatCount),
/// Unsupported / unexpected
Unknown,
/// upcase-word
UpcaseWord,
/// vi-yank-to
ViYankTo(Movement),
/// yank, vi-put
Yank { repeat: RepeatCount, anchor: Anchor },
/// yank-pop
YankPop,
/// moves cursor to the line above or switches to prev history entry if
/// the cursor is already on the first line
LineUpOrPreviousHistory(RepeatCount),
/// moves cursor to the line below or switches to next history entry if
/// the cursor is already on the last line
LineDownOrNextHistory(RepeatCount),
/// accepts the line when cursor is at the end of the text (non including
/// trailing whitespace), inserts newline character otherwise
AcceptOrInsertLine,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Movement {
/// Whole current line (not really a movement but a range)
WholeLine,
/// beginning-of-line
BeginningOfLine,
/// end-of-line
EndOfLine,
/// backward-word, vi-prev-word
BackwardWord { repeat: RepeatCount, word: Word }, // Backward until start of word
/// forward-word, vi-end-word, vi-next-word
ForwardWord {
repeat: RepeatCount,
at: At,
word: Word,
}, // Forward until start/end of word
/// vi-char-search
ViCharSearch {
repeat: RepeatCount,
search: CharSearch,
},
/// vi-first-print
ViFirstPrint,
/// backward-char
BackwardChar(RepeatCount),
/// forward-char
ForwardChar(RepeatCount),
/// move to the same column on the previous line
LineUp(RepeatCount),
/// move to the same column on the next line
LineDown(RepeatCount),
/// Whole user input (not really a movement but a range)
WholeBuffer,
/// beginning-of-buffer
BeginningOfBuffer,
/// end-of-buffer
EndOfBuffer,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
enum InputMode {
/// Vi Command/Alternate
Command,
/// Insert/Input mode
Insert,
/// Overwrite mode
Replace,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Word {
/// non-blanks characters
Big,
/// alphanumeric characters
Emacs,
/// alphanumeric (and '_') characters
Vi,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum At {
/// Start of word.
Start,
/// Before end of word.
BeforeEnd,
/// After end of word.
AfterEnd,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Anchor {
/// After cursor
After,
/// Before cursor
Before,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum CharSearch {
/// Forward search
Forward(char),
/// Forward search until
ForwardBefore(char),
/// Backward search
Backward(char),
/// Backward search until
BackwardAfter(char),
}
/// The set of modifier keys that were triggered along with a key press.
#[derive(Debug, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum NuModifiers {
/// Control modifier
#[serde(alias = "CTRL")]
Ctrl = 8,
/// Escape or Alt modifier
#[serde(alias = "ALT")]
Alt = 4,
/// Shift modifier
#[serde(alias = "SHIFT")]
Shift = 2,
/// No modifier
#[serde(alias = "NONE")]
None = 0,
/// Ctrl + Shift
#[serde(alias = "CTRL_SHIFT")]
CtrlShift = 10,
/// Alt + Shift
#[serde(alias = "ALT_SHIFT")]
AltShift = 6,
/// Ctrl + Alt
#[serde(alias = "CTRL_ALT")]
CtrlAlt = 12,
/// Ctrl + Alt + Shift
#[serde(alias = "CTRL_ALT_SHIFT")]
CtrlAltShift = 14,
}
/// The number of times one command should be repeated.
pub type RepeatCount = usize;
#[derive(Debug, Serialize, Deserialize)]
pub struct Keybinding {
key: KeyCode,
modifiers: Option<NuModifiers>,
binding: Cmd,
}
type Keybindings = Vec<Keybinding>;
pub(crate) fn load_keybindings(
rl: &mut rustyline::Editor<crate::shell::Helper>,
) -> Result<(), nu_errors::ShellError> {
let filename = nu_data::keybinding::keybinding_path()?;
let contents = std::fs::read_to_string(filename);
// Silently fail if there is no file there
if let Ok(contents) = contents {
let keybindings: Keybindings = serde_yaml::from_str(&contents)?;
// eprintln!("{:#?}", keybindings);
for keybinding in keybindings {
let (k, b) = convert_keybinding(keybinding);
// eprintln!("{:?} {:?}", k, b);
rl.bind_sequence(k, b);
}
}
Ok(())
}

View File

@ -1,3 +1,4 @@
<<<<<<< HEAD
pub mod app;
mod cli;
#[cfg(feature = "rustyline-support")]
@ -13,3 +14,18 @@ pub use crate::app::App;
pub use crate::cli::{parse_and_eval, register_plugins, run_script_file};
pub use nu_command::create_default_context;
=======
mod completions;
mod errors;
mod nu_highlight;
mod prompt;
mod syntax_highlight;
mod validation;
pub use completions::NuCompleter;
pub use errors::CliError;
pub use nu_highlight::NuHighlight;
pub use prompt::NushellPrompt;
pub use syntax_highlight::NuHighlighter;
pub use validation::NuValidator;
>>>>>>> 9259a56a28f1dd3a4b720ad815aa19c6eaf6adce

View File

@ -1,276 +0,0 @@
use nu_engine::EvaluationContext;
use nu_errors::ShellError;
use std::error::Error;
#[allow(unused_imports)]
use std::sync::atomic::Ordering;
#[allow(unused_imports)]
use nu_engine::script::LineResult;
#[cfg(feature = "rustyline-support")]
use crate::keybinding::{convert_keyevent, KeyCode};
#[cfg(feature = "rustyline-support")]
use crate::shell::Helper;
#[cfg(feature = "rustyline-support")]
use rustyline::{
self,
config::Configurer,
config::{ColorMode, CompletionType, Config},
error::ReadlineError,
line_buffer::LineBuffer,
At, Cmd, ConditionalEventHandler, Editor, EventHandler, Modifiers, Movement, Word,
};
#[cfg(feature = "rustyline-support")]
pub fn convert_rustyline_result_to_string(input: Result<String, ReadlineError>) -> LineResult {
match input {
Ok(s) if s == "history -c" || s == "history --clear" => LineResult::ClearHistory,
Ok(s) => LineResult::Success(s),
Err(ReadlineError::Interrupted) => LineResult::CtrlC,
Err(ReadlineError::Eof) => LineResult::CtrlD,
Err(err) => {
eprintln!("Error: {:?}", err);
LineResult::Break
}
}
}
#[derive(Clone)]
#[cfg(feature = "rustyline-support")]
struct PartialCompleteHintHandler;
#[cfg(feature = "rustyline-support")]
impl ConditionalEventHandler for PartialCompleteHintHandler {
fn handle(
&self,
_evt: &rustyline::Event,
_n: rustyline::RepeatCount,
_positive: bool,
ctx: &rustyline::EventContext,
) -> Option<Cmd> {
Some(match ctx.hint_text() {
Some(hint_text) if ctx.pos() == ctx.line().len() => {
let mut line_buffer = LineBuffer::with_capacity(hint_text.len());
line_buffer.update(hint_text, 0);
line_buffer.move_to_next_word(At::AfterEnd, Word::Vi, 1);
let text = hint_text[0..line_buffer.pos()].to_string();
Cmd::Insert(1, text)
}
_ => Cmd::Move(Movement::ForwardWord(1, At::AfterEnd, Word::Vi)),
})
}
}
#[cfg(feature = "rustyline-support")]
pub fn default_rustyline_editor_configuration() -> Editor<Helper> {
#[cfg(windows)]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
#[cfg(not(windows))]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
let config = Config::builder()
.check_cursor_position(true)
.color_mode(ColorMode::Forced)
.history_ignore_dups(false)
.max_history_size(10_000)
.build();
let mut rl: Editor<_> = Editor::with_config(config);
// add key bindings to move over a whole word with Ctrl+ArrowLeft and Ctrl+ArrowRight
//M modifier, E KeyEvent, K KeyCode
rl.bind_sequence(
convert_keyevent(KeyCode::Left, Some(Modifiers::CTRL)),
Cmd::Move(Movement::BackwardWord(1, Word::Vi)),
);
rl.bind_sequence(
convert_keyevent(KeyCode::Right, Some(Modifiers::CTRL)),
EventHandler::Conditional(Box::new(PartialCompleteHintHandler)),
);
// workaround for multiline-paste hang in rustyline (see https://github.com/kkawakam/rustyline/issues/202)
rl.bind_sequence(
convert_keyevent(KeyCode::BracketedPasteStart, None),
rustyline::Cmd::Noop,
);
// Let's set the defaults up front and then override them later if the user indicates
// defaults taken from here https://github.com/kkawakam/rustyline/blob/2fe886c9576c1ea13ca0e5808053ad491a6fe049/src/config.rs#L150-L167
rl.set_max_history_size(100);
rl.set_history_ignore_dups(true);
rl.set_history_ignore_space(false);
rl.set_completion_type(DEFAULT_COMPLETION_MODE);
rl.set_completion_prompt_limit(100);
rl.set_keyseq_timeout(-1);
rl.set_edit_mode(rustyline::config::EditMode::Emacs);
rl.set_auto_add_history(false);
rl.set_bell_style(rustyline::config::BellStyle::default());
rl.set_color_mode(rustyline::ColorMode::Enabled);
rl.set_tab_stop(8);
if let Err(e) = crate::keybinding::load_keybindings(&mut rl) {
println!("Error loading keybindings: {:?}", e);
}
rl
}
#[cfg(feature = "rustyline-support")]
pub fn configure_rustyline_editor(
rl: &mut Editor<Helper>,
config: &dyn nu_data::config::Conf,
) -> Result<(), ShellError> {
#[cfg(windows)]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::Circular;
#[cfg(not(windows))]
const DEFAULT_COMPLETION_MODE: CompletionType = CompletionType::List;
if let Some(line_editor_vars) = config.var("line_editor") {
for (idx, value) in line_editor_vars.row_entries() {
match idx.as_ref() {
"max_history_size" => {
if let Ok(max_history_size) = value.as_u64() {
rl.set_max_history_size(max_history_size as usize);
}
}
"history_duplicates" => {
// history_duplicates = match value.as_string() {
// Ok(s) if s.to_lowercase() == "alwaysadd" => {
// rustyline::config::HistoryDuplicates::AlwaysAdd
// }
// Ok(s) if s.to_lowercase() == "ignoreconsecutive" => {
// rustyline::config::HistoryDuplicates::IgnoreConsecutive
// }
// _ => rustyline::config::HistoryDuplicates::AlwaysAdd,
// };
if let Ok(history_duplicates) = value.as_bool() {
rl.set_history_ignore_dups(history_duplicates);
}
}
"history_ignore_space" => {
if let Ok(history_ignore_space) = value.as_bool() {
rl.set_history_ignore_space(history_ignore_space);
}
}
"completion_type" => {
let completion_type = match value.as_string() {
Ok(s) if s.to_lowercase() == "circular" => {
rustyline::config::CompletionType::Circular
}
Ok(s) if s.to_lowercase() == "list" => {
rustyline::config::CompletionType::List
}
#[cfg(all(unix, feature = "with-fuzzy"))]
Ok(s) if s.to_lowercase() == "fuzzy" => {
rustyline::config::CompletionType::Fuzzy
}
_ => DEFAULT_COMPLETION_MODE,
};
rl.set_completion_type(completion_type);
}
"completion_prompt_limit" => {
if let Ok(completion_prompt_limit) = value.as_u64() {
rl.set_completion_prompt_limit(completion_prompt_limit as usize);
}
}
"keyseq_timeout_ms" => {
if let Ok(keyseq_timeout_ms) = value.as_u64() {
rl.set_keyseq_timeout(keyseq_timeout_ms as i32);
}
}
"edit_mode" => {
let edit_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "vi" => rustyline::config::EditMode::Vi,
Ok(s) if s.to_lowercase() == "emacs" => rustyline::config::EditMode::Emacs,
_ => rustyline::config::EditMode::Emacs,
};
rl.set_edit_mode(edit_mode);
// Note: When edit_mode is Emacs, the keyseq_timeout_ms is set to -1
// no matter what you may have configured. This is so that key chords
// can be applied without having to do them in a given timeout. So,
// it essentially turns off the keyseq timeout.
}
"auto_add_history" => {
if let Ok(auto_add_history) = value.as_bool() {
rl.set_auto_add_history(auto_add_history);
}
}
"bell_style" => {
let bell_style = match value.as_string() {
Ok(s) if s.to_lowercase() == "audible" => {
rustyline::config::BellStyle::Audible
}
Ok(s) if s.to_lowercase() == "none" => rustyline::config::BellStyle::None,
Ok(s) if s.to_lowercase() == "visible" => {
rustyline::config::BellStyle::Visible
}
_ => rustyline::config::BellStyle::default(),
};
rl.set_bell_style(bell_style);
}
"color_mode" => {
let color_mode = match value.as_string() {
Ok(s) if s.to_lowercase() == "enabled" => rustyline::ColorMode::Enabled,
Ok(s) if s.to_lowercase() == "forced" => rustyline::ColorMode::Forced,
Ok(s) if s.to_lowercase() == "disabled" => rustyline::ColorMode::Disabled,
_ => rustyline::ColorMode::Enabled,
};
rl.set_color_mode(color_mode);
}
"tab_stop" => {
if let Ok(tab_stop) = value.as_u64() {
rl.set_tab_stop(tab_stop as usize);
}
}
_ => (),
}
}
}
Ok(())
}
#[cfg(feature = "rustyline-support")]
pub fn nu_line_editor_helper(
context: &EvaluationContext,
config: &dyn nu_data::config::Conf,
) -> crate::shell::Helper {
let hinter = rustyline_hinter(config);
crate::shell::Helper::new(context.clone(), hinter)
}
#[cfg(feature = "rustyline-support")]
pub fn rustyline_hinter(
config: &dyn nu_data::config::Conf,
) -> Option<rustyline::hint::HistoryHinter> {
if let Some(line_editor_vars) = config.var("line_editor") {
for (idx, value) in line_editor_vars.row_entries() {
if idx == "show_hints" && value.as_bool() == Ok(false) {
return None;
}
}
}
Some(rustyline::hint::HistoryHinter {})
}
pub fn configure_ctrl_c(_context: &EvaluationContext) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "ctrlc")]
{
let cc = _context.ctrl_c().clone();
ctrlc::set_handler(move || {
cc.store(true, Ordering::SeqCst);
})?;
if _context.ctrl_c().load(Ordering::SeqCst) {
_context.ctrl_c().store(false, Ordering::SeqCst);
}
}
Ok(())
}

View File

@ -0,0 +1,63 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Value};
use reedline::Highlighter;
#[derive(Clone)]
pub struct NuHighlight;
impl Command for NuHighlight {
fn name(&self) -> &str {
"nu-highlight"
}
fn signature(&self) -> Signature {
Signature::build("nu-highlight").category(Category::Strings)
}
fn usage(&self) -> &str {
"Syntax highlight the input string."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.clone();
let config = stack.get_config()?;
let highlighter = crate::NuHighlighter {
engine_state,
config,
};
input.map(
move |x| match x.as_string() {
Ok(line) => {
let highlights = highlighter.highlight(&line);
Value::String {
val: highlights.render_simple(),
span: head,
}
}
Err(err) => Value::Error { error: err },
},
ctrlc,
)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Describe the type of a string",
example: "'let x = 3' | nu-highlight",
result: None,
}]
}
}

143
crates/nu-cli/src/prompt.rs Normal file
View File

@ -0,0 +1,143 @@
use reedline::DefaultPrompt;
use {
reedline::{
Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode,
},
std::borrow::Cow,
};
/// Nushell prompt definition
#[derive(Clone)]
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,
}
impl Default for NushellPrompt {
fn default() -> Self {
NushellPrompt::new()
}
}
impl NushellPrompt {
pub fn new() -> 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(),
}
}
pub fn update_prompt_left(&mut self, prompt_string: Option<String>) {
self.left_prompt_string = prompt_string;
}
pub fn update_prompt_right(&mut self, prompt_string: Option<String>) {
self.right_prompt_string = prompt_string;
}
pub fn update_prompt_indicator(&mut self, prompt_indicator_string: String) {
self.default_prompt_indicator = prompt_indicator_string;
}
pub fn update_prompt_vi_insert(&mut self, prompt_vi_insert_string: String) {
self.default_vi_insert_prompt_indicator = prompt_vi_insert_string;
}
pub fn update_prompt_vi_normal(&mut self, prompt_vi_normal_string: String) {
self.default_vi_normal_prompt_indicator = prompt_vi_normal_string;
}
pub fn update_prompt_multiline(&mut self, prompt_multiline_indicator_string: String) {
self.default_multiline_indicator = prompt_multiline_indicator_string;
}
pub fn update_all_prompt_strings(
&mut self,
left_prompt_string: Option<String>,
right_prompt_string: Option<String>,
prompt_indicator_string: String,
prompt_multiline_indicator_string: String,
prompt_vi: (String, 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_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 {
format!("({})", str)
}
}
impl Prompt for NushellPrompt {
fn render_prompt_left(&self) -> Cow<str> {
if let Some(prompt_string) = &self.left_prompt_string {
prompt_string.replace("\n", "\r\n").into()
} else {
let default = DefaultPrompt::new();
default
.render_prompt_left()
.to_string()
.replace("\n", "\r\n")
.into()
}
}
fn render_prompt_right(&self) -> Cow<str> {
if let Some(prompt_string) = &self.right_prompt_string {
prompt_string.replace("\n", "\r\n").into()
} else {
let default = DefaultPrompt::new();
default
.render_prompt_right()
.to_string()
.replace("\n", "\r\n")
.into()
}
}
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
match edit_mode {
PromptEditMode::Default => self.default_prompt_indicator.as_str().into(),
PromptEditMode::Emacs => self.default_prompt_indicator.as_str().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(),
},
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())
}
fn render_prompt_history_search_indicator(
&self,
history_search: PromptHistorySearch,
) -> Cow<str> {
let prefix = match history_search.status {
PromptHistorySearchStatus::Passing => "",
PromptHistorySearchStatus::Failing => "failing ",
};
Cow::Owned(format!(
"({}reverse-search: {})",
prefix, history_search.term
))
}
}

View File

@ -1,234 +0,0 @@
use nu_ansi_term::Color;
use nu_completion::NuCompleter;
use nu_engine::{DefaultPalette, EvaluationContext, Painter};
use nu_source::{Tag, Tagged};
use std::borrow::Cow::{self, Owned};
pub struct Helper {
completer: NuCompleter,
hinter: Option<rustyline::hint::HistoryHinter>,
context: EvaluationContext,
pub colored_prompt: String,
validator: NuValidator,
}
impl Helper {
pub(crate) fn new(
context: EvaluationContext,
hinter: Option<rustyline::hint::HistoryHinter>,
) -> Helper {
Helper {
completer: NuCompleter {},
hinter,
context,
colored_prompt: String::new(),
validator: NuValidator {},
}
}
}
use nu_protocol::{SignatureRegistry, VariableRegistry};
struct CompletionContext<'a>(&'a EvaluationContext);
impl<'a> nu_completion::CompletionContext for CompletionContext<'a> {
fn signature_registry(&self) -> &dyn SignatureRegistry {
&self.0.scope
}
fn scope(&self) -> &dyn nu_parser::ParserScope {
&self.0.scope
}
fn source(&self) -> &EvaluationContext {
self.as_ref()
}
fn variable_registry(&self) -> &dyn VariableRegistry {
self.0
}
}
impl<'a> AsRef<EvaluationContext> for CompletionContext<'a> {
fn as_ref(&self) -> &EvaluationContext {
self.0
}
}
pub struct CompletionSuggestion(nu_completion::Suggestion);
impl rustyline::completion::Candidate for CompletionSuggestion {
fn display(&self) -> &str {
&self.0.display
}
fn replacement(&self) -> &str {
&self.0.replacement
}
}
impl rustyline::completion::Completer for Helper {
type Candidate = CompletionSuggestion;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> Result<(usize, Vec<Self::Candidate>), rustyline::error::ReadlineError> {
let ctx = CompletionContext(&self.context);
let (position, suggestions) = self.completer.complete(line, pos, &ctx);
let suggestions = suggestions.into_iter().map(CompletionSuggestion).collect();
Ok((position, suggestions))
}
fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) {
let end = line.pos();
line.replace(start..end, elected)
}
}
impl rustyline::hint::Hinter for Helper {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
match &self.hinter {
Some(the_hinter) => the_hinter.hint(line, pos, ctx),
None => Some("".to_string()),
}
}
}
impl rustyline::highlight::Highlighter for Helper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> Cow<'b, str> {
use std::borrow::Cow::Borrowed;
if default {
Borrowed(&self.colored_prompt)
} else {
Borrowed(prompt)
}
}
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned(Color::DarkGray.prefix().to_string() + hint + nu_ansi_term::ansi::RESET)
}
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
let cfg = &self.context.configs().lock();
if let Some(palette) = &cfg.syntax_config {
Painter::paint_string(line, &self.context.scope, palette)
} else {
Painter::paint_string(line, &self.context.scope, &DefaultPalette {})
}
}
fn highlight_char(&self, _line: &str, _pos: usize) -> bool {
true
}
}
impl rustyline::validate::Validator for Helper {
fn validate(
&self,
ctx: &mut rustyline::validate::ValidationContext,
) -> rustyline::Result<rustyline::validate::ValidationResult> {
self.validator.validate(ctx)
}
fn validate_while_typing(&self) -> bool {
self.validator.validate_while_typing()
}
}
struct NuValidator {}
impl rustyline::validate::Validator for NuValidator {
fn validate(
&self,
ctx: &mut rustyline::validate::ValidationContext,
) -> rustyline::Result<rustyline::validate::ValidationResult> {
let src = ctx.input();
let (tokens, err) = nu_parser::lex(src, 0, nu_parser::NewlineMode::Normal);
if let Some(err) = err {
if let nu_errors::ParseErrorReason::Eof { .. } = err.reason() {
return Ok(rustyline::validate::ValidationResult::Incomplete);
}
}
let (_, err) = nu_parser::parse_block(tokens);
if let Some(err) = err {
if let nu_errors::ParseErrorReason::Eof { .. } = err.reason() {
return Ok(rustyline::validate::ValidationResult::Incomplete);
}
}
Ok(rustyline::validate::ValidationResult::Valid(None))
}
}
#[allow(unused)]
fn vec_tag<T>(input: Vec<Tagged<T>>) -> Option<Tag> {
let mut iter = input.iter();
let first = iter.next()?.tag.clone();
let last = iter.last();
Some(match last {
None => first,
Some(last) => first.until(&last.tag),
})
}
impl rustyline::Helper for Helper {}
#[cfg(test)]
mod tests {
use super::*;
use nu_engine::EvaluationContext;
use rustyline::completion::Completer;
use rustyline::line_buffer::LineBuffer;
#[ignore]
#[test]
fn closing_quote_should_replaced() {
let text = "cd \"folder with spaces\\subdirectory\\\"";
let replacement = "\"folder with spaces\\subdirectory\\subsubdirectory\\\"";
let mut buffer = LineBuffer::with_capacity(256);
buffer.insert_str(0, text);
buffer.set_pos(text.len() - 1);
let helper = Helper::new(EvaluationContext::basic(), None);
helper.update(&mut buffer, "cd ".len(), replacement);
assert_eq!(
buffer.as_str(),
"cd \"folder with spaces\\subdirectory\\subsubdirectory\\\""
);
}
#[ignore]
#[test]
fn replacement_with_cursor_in_text() {
let text = "cd \"folder with spaces\\subdirectory\\\"";
let replacement = "\"folder with spaces\\subdirectory\\subsubdirectory\\\"";
let mut buffer = LineBuffer::with_capacity(256);
buffer.insert_str(0, text);
buffer.set_pos(text.len() - 30);
let helper = Helper::new(EvaluationContext::basic(), None);
helper.update(&mut buffer, "cd ".len(), replacement);
assert_eq!(
buffer.as_str(),
"cd \"folder with spaces\\subdirectory\\subsubdirectory\\\""
);
}
}

View File

@ -0,0 +1,199 @@
use log::trace;
use nu_ansi_term::Style;
use nu_color_config::get_shape_color;
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::engine::{EngineState, StateWorkingSet};
use nu_protocol::Config;
use reedline::{Highlighter, StyledText};
pub struct NuHighlighter {
pub engine_state: EngineState,
pub config: Config,
}
impl Highlighter for NuHighlighter {
fn highlight(&self, line: &str) -> StyledText {
trace!("highlighting: {}", line);
let (shapes, global_span_offset) = {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let (block, _) = parse(&mut working_set, None, line.as_bytes(), false);
let shapes = flatten_block(&working_set, &block);
(shapes, self.engine_state.next_span_start())
};
let mut output = StyledText::default();
let mut last_seen_span = global_span_offset;
for shape in &shapes {
if shape.0.end <= last_seen_span
|| last_seen_span < global_span_offset
|| shape.0.start < global_span_offset
{
// We've already output something for this span
// so just skip this one
continue;
}
if shape.0.start > last_seen_span {
let gap = line
[(last_seen_span - global_span_offset)..(shape.0.start - global_span_offset)]
.to_string();
output.push((Style::new(), gap));
}
let next_token = line
[(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)]
.to_string();
match shape.1 {
FlatShape::Garbage => output.push((
// nushell Garbage
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::Nothing => output.push((
// nushell Nothing
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::Bool => {
// nushell ?
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Int => {
// nushell Int
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Float => {
// nushell Decimal
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Range => output.push((
// nushell DotDot ?
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::InternalCall => output.push((
// nushell InternalCommand
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::External => {
// nushell ExternalCommand
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::ExternalArg => {
// nushell ExternalWord
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Literal => {
// nushell ?
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Operator => output.push((
// nushell Operator
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::Signature => output.push((
// nushell ?
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::String => {
// nushell String
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::StringInterpolation => {
// nushell ???
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::List => {
// nushell ???
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Table => {
// nushell ???
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Record => {
// nushell ???
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Block => {
// nushell ???
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Filepath => output.push((
// nushell Path
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::GlobPattern => output.push((
// nushell GlobPattern
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::Variable => output.push((
// nushell Variable
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
FlatShape::Flag => {
// nushell Flag
output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
))
}
FlatShape::Custom(..) => output.push((
get_shape_color(shape.1.to_string(), &self.config),
next_token,
)),
}
last_seen_span = shape.0.end;
}
let remainder = line[(last_seen_span - global_span_offset)..].to_string();
if !remainder.is_empty() {
output.push((Style::new(), remainder));
}
output
}
}

View File

@ -0,0 +1,20 @@
use nu_parser::{parse, ParseError};
use nu_protocol::engine::{EngineState, StateWorkingSet};
use reedline::{ValidationResult, Validator};
pub struct NuValidator {
pub engine_state: EngineState,
}
impl Validator for NuValidator {
fn validate(&self, line: &str) -> ValidationResult {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let (_, err) = parse(&mut working_set, None, line.as_bytes(), false);
if matches!(err, Some(ParseError::UnexpectedEof(..))) {
ValidationResult::Incomplete
} else {
ValidationResult::Complete
}
}
}