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,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
}
}
}