From 20eb348896463e3fba12172fd999df7d8a65850a Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Tue, 18 Jan 2022 08:48:28 +0000 Subject: [PATCH] simple keybinding parsing (#768) --- Cargo.lock | 2 +- crates/nu-protocol/src/config.rs | 190 +++++- src/config_files.rs | 83 +++ src/eval_file.rs | 167 +++++ src/fuzzy_completion.rs | 57 ++ src/main.rs | 1033 +----------------------------- src/prompt_update.rs | 99 +++ src/reedline_config.rs | 104 +++ src/repl.rs | 257 ++++++++ src/utils.rs | 395 ++++++++++++ 10 files changed, 1347 insertions(+), 1040 deletions(-) create mode 100644 src/config_files.rs create mode 100644 src/eval_file.rs create mode 100644 src/fuzzy_completion.rs create mode 100644 src/prompt_update.rs create mode 100644 src/reedline_config.rs create mode 100644 src/repl.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 077c07308b..9eccec9c30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2851,7 +2851,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.2.0" -source = "git+https://github.com/nushell/reedline?branch=main#32338dc18aaa1633bf636717c334f22dae8e374a" +source = "git+https://github.com/nushell/reedline?branch=main#b0ff0eb4d1f062a03b76ef0ac28a2ccaada32a52" dependencies = [ "chrono", "crossterm", diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 8fe530e544..cdb54ef706 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -38,6 +38,37 @@ impl EnvConversion { } } +/// Definition of a parsed keybinding from the config object +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ParsedKeybinding { + pub modifier: String, + pub keycode: String, + pub event: EventType, + pub mode: EventMode, +} + +impl Default for ParsedKeybinding { + fn default() -> Self { + Self { + modifier: "".to_string(), + keycode: "".to_string(), + event: EventType::Single("".to_string()), + mode: EventMode::Emacs, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum EventType { + Single(String), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum EventMode { + Emacs, + Vi, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { pub filesize_metric: bool, @@ -55,6 +86,7 @@ pub struct Config { pub max_history_size: i64, pub log_level: String, pub menu_config: HashMap, + pub keybindings: Vec, } impl Default for Config { @@ -75,6 +107,7 @@ impl Default for Config { max_history_size: 1000, log_level: String::new(), menu_config: HashMap::new(), + keybindings: Vec::new(), } } } @@ -161,6 +194,7 @@ impl Value { "menu_config" => { config.menu_config = create_map(value, &config)?; } + "keybindings" => config.keybindings = create_keybindings(value, &config)?, _ => {} } } @@ -180,30 +214,8 @@ fn create_map(value: &Value, config: &Config) -> Result, vals: inner_vals, span, } => { - // make a string from our config.color_config section that - // looks like this: { fg: "#rrggbb" bg: "#rrggbb" attr: "abc", } - // the real key here was to have quotes around the values but not - // require them around the keys. - - // maybe there's a better way to generate this but i'm not sure - // what it is. - let key = k.to_string(); - let val: String = inner_cols - .iter() - .zip(inner_vals) - .map(|(x, y)| { - let clony = y.clone(); - format!("{}: \"{}\" ", x, clony.into_string(", ", config)) - }) - .collect(); - - // now insert the braces at the front and the back to fake the json string - let val = Value::String { - val: format!("{{{}}}", val), - span: *span, - }; - - hm.insert(key, val); + let val = color_value_string(span, inner_cols, inner_vals, config); + hm.insert(k.to_string(), val); } _ => { hm.insert(k.to_string(), v.clone()); @@ -213,3 +225,133 @@ fn create_map(value: &Value, config: &Config) -> Result, Ok(hm) } + +fn color_value_string( + span: &Span, + inner_cols: &[String], + inner_vals: &[Value], + config: &Config, +) -> Value { + // make a string from our config.color_config section that + // looks like this: { fg: "#rrggbb" bg: "#rrggbb" attr: "abc", } + // the real key here was to have quotes around the values but not + // require them around the keys. + + // maybe there's a better way to generate this but i'm not sure + // what it is. + let val: String = inner_cols + .iter() + .zip(inner_vals) + .map(|(x, y)| { + let clony = y.clone(); + format!("{}: \"{}\" ", x, clony.into_string(", ", config)) + }) + .collect(); + + // now insert the braces at the front and the back to fake the json string + Value::String { + val: format!("{{{}}}", val), + span: *span, + } +} + +// Parses the config object to extract the strings that will compose a keybinding for reedline +fn create_keybindings(value: &Value, config: &Config) -> Result, ShellError> { + match value { + Value::Record { cols, vals, .. } => { + let mut keybinding = ParsedKeybinding::default(); + + for (col, val) in cols.iter().zip(vals.iter()) { + match col.as_str() { + "modifier" => keybinding.modifier = val.clone().into_string("", config), + "keycode" => keybinding.keycode = val.clone().into_string("", config), + "mode" => { + keybinding.mode = match val.clone().into_string("", config).as_str() { + "emacs" => EventMode::Emacs, + "vi" => EventMode::Vi, + e => { + return Err(ShellError::UnsupportedConfigValue( + "emacs or vi".to_string(), + e.to_string(), + val.span()?, + )) + } + }; + } + "event" => match val { + Value::Record { + cols: event_cols, + vals: event_vals, + span: event_span, + } => { + let event_type_idx = event_cols + .iter() + .position(|key| key == "type") + .ok_or_else(|| { + ShellError::MissingConfigValue("type".to_string(), *event_span) + })?; + + let event_idx = event_cols + .iter() + .position(|key| key == "event") + .ok_or_else(|| { + ShellError::MissingConfigValue("event".to_string(), *event_span) + })?; + + let event_type = + event_vals[event_type_idx].clone().into_string("", config); + + // Extracting the event type information from the record based on the type + match event_type.as_str() { + "single" => { + let event_value = + event_vals[event_idx].clone().into_string("", config); + + keybinding.event = EventType::Single(event_value) + } + e => { + return Err(ShellError::UnsupportedConfigValue( + "single".to_string(), + e.to_string(), + *event_span, + )) + } + }; + } + e => { + return Err(ShellError::UnsupportedConfigValue( + "record type".to_string(), + format!("{:?}", e.get_type()), + e.span()?, + )) + } + }, + "name" => {} // don't need to store name + e => { + return Err(ShellError::UnsupportedConfigValue( + "name, mode, modifier, keycode or event".to_string(), + e.to_string(), + val.span()?, + )) + } + } + } + + Ok(vec![keybinding]) + } + Value::List { vals, .. } => { + let res = vals + .iter() + .map(|inner_value| create_keybindings(inner_value, config)) + .collect::>, ShellError>>(); + + let res = res? + .into_iter() + .flatten() + .collect::>(); + + Ok(res) + } + _ => Ok(Vec::new()), + } +} diff --git a/src/config_files.rs b/src/config_files.rs new file mode 100644 index 0000000000..727cbed376 --- /dev/null +++ b/src/config_files.rs @@ -0,0 +1,83 @@ +use crate::utils::{eval_source, report_error}; +use nu_protocol::engine::{EngineState, Stack, StateDelta, StateWorkingSet}; +use std::path::PathBuf; + +const NUSHELL_FOLDER: &str = "nushell"; +const PLUGIN_FILE: &str = "plugin.nu"; +const CONFIG_FILE: &str = "config.nu"; +const HISTORY_FILE: &str = "history.txt"; + +pub(crate) fn read_plugin_file(engine_state: &mut EngineState, stack: &mut Stack) { + // Reading signatures from signature file + // The plugin.nu file stores the parsed signature collected from each registered plugin + if let Some(mut plugin_path) = nu_path::config_dir() { + // Path to store plugins signatures + plugin_path.push(NUSHELL_FOLDER); + plugin_path.push(PLUGIN_FILE); + engine_state.plugin_signatures = Some(plugin_path.clone()); + + let plugin_filename = plugin_path.to_string_lossy().to_owned(); + + if let Ok(contents) = std::fs::read_to_string(&plugin_path) { + eval_source(engine_state, stack, &contents, &plugin_filename); + } + } +} + +pub(crate) fn read_config_file(engine_state: &mut EngineState, stack: &mut Stack) { + // Load config startup file + if let Some(mut config_path) = nu_path::config_dir() { + config_path.push(NUSHELL_FOLDER); + + // Create config directory if it does not exist + if !config_path.exists() { + if let Err(err) = std::fs::create_dir_all(&config_path) { + eprintln!("Failed to create config directory: {}", err); + } + } else { + config_path.push(CONFIG_FILE); + + if config_path.exists() { + // FIXME: remove this message when we're ready + println!("Loading config from: {:?}", config_path); + let config_filename = config_path.to_string_lossy().to_owned(); + + if let Ok(contents) = std::fs::read_to_string(&config_path) { + eval_source(engine_state, stack, &contents, &config_filename); + // Merge the delta in case env vars changed in the config + match nu_engine::env::current_dir(engine_state, stack) { + Ok(cwd) => { + if let Err(e) = + engine_state.merge_delta(StateDelta::new(), Some(stack), cwd) + { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + } + } + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + } + } + } + } + } + } +} + +pub(crate) fn create_history_path() -> Option { + nu_path::config_dir().and_then(|mut history_path| { + history_path.push(NUSHELL_FOLDER); + history_path.push(HISTORY_FILE); + + if !history_path.exists() { + // Creating an empty file to store the history + match std::fs::File::create(&history_path) { + Ok(_) => Some(history_path), + Err(_) => None, + } + } else { + Some(history_path) + } + }) +} diff --git a/src/eval_file.rs b/src/eval_file.rs new file mode 100644 index 0000000000..54ff9c11df --- /dev/null +++ b/src/eval_file.rs @@ -0,0 +1,167 @@ +use log::trace; +use miette::{IntoDiagnostic, Result}; +use nu_engine::{convert_env_values, eval_block}; +use nu_parser::parse; +use nu_protocol::{ + engine::{EngineState, StateDelta, StateWorkingSet}, + Config, PipelineData, Span, Value, CONFIG_VARIABLE_ID, +}; +use std::path::PathBuf; + +use crate::utils::{gather_parent_env_vars, report_error}; + +/// Main function used when a file path is found as argument for nu +pub(crate) fn evaluate( + path: String, + init_cwd: PathBuf, + engine_state: &mut EngineState, +) -> Result<()> { + // First, set up env vars as strings only + gather_parent_env_vars(engine_state); + + let file = std::fs::read(&path).into_diagnostic()?; + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(engine_state); + trace!("parsing file: {}", path); + + let (output, err) = parse(&mut working_set, Some(&path), &file, false); + if let Some(err) = err { + report_error(&working_set, &err); + + std::process::exit(1); + } + (output, working_set.render()) + }; + + if let Err(err) = engine_state.merge_delta(delta, None, &init_cwd) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } + + let mut stack = nu_protocol::engine::Stack::new(); + + // Set up our initial config to start from + stack.vars.insert( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: Span { start: 0, end: 0 }, + }, + ); + + let config = match stack.get_config() { + Ok(config) => config, + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &e); + Config::default() + } + }; + + // Merge the delta in case env vars changed in the config + match nu_engine::env::current_dir(engine_state, &stack) { + Ok(cwd) => { + if let Err(e) = engine_state.merge_delta(StateDelta::new(), Some(&mut stack), cwd) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + } + } + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + } + } + + // Translate environment variables from Strings to Values + if let Some(e) = convert_env_values(engine_state, &stack, &config) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + std::process::exit(1); + } + + match eval_block( + engine_state, + &mut stack, + &block, + PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored + ) { + Ok(pipeline_data) => { + for item in pipeline_data { + if let Value::Error { error } = item { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &error); + + std::process::exit(1); + } + println!("{}", item.into_string("\n", &config)); + } + + // Next, let's check if there are any flags we want to pass to the main function + let args: Vec = std::env::args().skip(2).collect(); + + if args.is_empty() && engine_state.find_decl(b"main").is_none() { + return Ok(()); + } + + let args = format!("main {}", args.join(" ")).as_bytes().to_vec(); + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(engine_state); + let (output, err) = parse(&mut working_set, Some(""), &args, false); + if let Some(err) = err { + report_error(&working_set, &err); + + std::process::exit(1); + } + (output, working_set.render()) + }; + + let cwd = nu_engine::env::current_dir_str(engine_state, &stack)?; + + if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } + + match eval_block( + engine_state, + &mut stack, + &block, + PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored + ) { + Ok(pipeline_data) => { + for item in pipeline_data { + if let Value::Error { error } = item { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &error); + + std::process::exit(1); + } + println!("{}", item.into_string("\n", &config)); + } + } + Err(err) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &err); + + std::process::exit(1); + } + } + } + Err(err) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &err); + + std::process::exit(1); + } + } + + Ok(()) +} diff --git a/src/fuzzy_completion.rs b/src/fuzzy_completion.rs new file mode 100644 index 0000000000..1f8f1cd6e0 --- /dev/null +++ b/src/fuzzy_completion.rs @@ -0,0 +1,57 @@ +use dialoguer::{ + console::{Style, Term}, + theme::ColorfulTheme, + Select, +}; +use reedline::{Completer, CompletionActionHandler, LineBuffer}; + +pub(crate) struct FuzzyCompletion { + pub completer: Box, +} + +impl CompletionActionHandler for FuzzyCompletion { + fn handle(&mut self, present_buffer: &mut LineBuffer) { + let completions = self + .completer + .complete(present_buffer.get_buffer(), present_buffer.offset()); + + if completions.is_empty() { + // do nothing + } else if completions.len() == 1 { + let span = completions[0].0; + + let mut offset = present_buffer.offset(); + offset += completions[0].1.len() - (span.end - span.start); + + // TODO improve the support for multiline replace + present_buffer.replace(span.start..span.end, &completions[0].1); + present_buffer.set_insertion_point(offset); + } else { + let selections: Vec<_> = completions.iter().map(|(_, string)| string).collect(); + + let _ = crossterm::terminal::disable_raw_mode(); + println!(); + let theme = ColorfulTheme { + active_item_style: Style::new().for_stderr().on_green().black(), + ..Default::default() + }; + let result = Select::with_theme(&theme) + .default(0) + .items(&selections[..]) + .interact_on_opt(&Term::stdout()) + .unwrap_or(None); + let _ = crossterm::terminal::enable_raw_mode(); + + if let Some(result) = result { + let span = completions[result].0; + + let mut offset = present_buffer.offset(); + offset += completions[result].1.len() - (span.end - span.start); + + // TODO improve the support for multiline replace + present_buffer.replace(span.start..span.end, &completions[result].1); + present_buffer.set_insertion_point(offset); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 6a447422d2..b7de4ca863 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,22 @@ -use crossterm::event::{KeyCode, KeyModifiers}; -#[cfg(windows)] -use crossterm_winapi::{ConsoleMode, Handle}; -use log::trace; -use miette::{IntoDiagnostic, Result}; -use nu_cli::{CliError, NuCompleter, NuHighlighter, NuValidator, NushellPrompt}; -use nu_color_config::{get_color_config, lookup_ansi_color_style}; -use nu_command::create_default_context; -use nu_engine::{convert_env_values, eval_block}; -use nu_parser::{lex, parse, trim_quotes, Token, TokenContents}; -use nu_protocol::{ - ast::Call, - engine::{EngineState, Stack, StateDelta, StateWorkingSet}, - Config, PipelineData, ShellError, Span, Value, CONFIG_VARIABLE_ID, -}; -use reedline::{ - default_emacs_keybindings, ContextMenuInput, DefaultHinter, EditCommand, Emacs, Prompt, - ReedlineEvent, Vi, -}; -use std::{ - io::Write, - path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; +mod config_files; +mod eval_file; +mod logger; +mod prompt_update; +mod reedline_config; +mod repl; +mod utils; + +// mod fuzzy_completion; #[cfg(test)] mod tests; -mod logger; - -// Name of environment variable where the prompt could be stored -const PROMPT_COMMAND: &str = "PROMPT_COMMAND"; -const PROMPT_COMMAND_RIGHT: &str = "PROMPT_COMMAND_RIGHT"; -const PROMPT_INDICATOR: &str = "PROMPT_INDICATOR"; -const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT"; -const PROMPT_INDICATOR_VI_VISUAL: &str = "PROMPT_INDICATOR_VI_VISUAL"; -const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR"; +use miette::Result; +use nu_command::create_default_context; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; fn main() -> Result<()> { // miette::set_panic_hook(); @@ -48,7 +27,7 @@ fn main() -> Result<()> { })); // Get initial current working directory. - let init_cwd = get_init_cwd(); + let init_cwd = utils::get_init_cwd(); let mut engine_state = create_default_context(&init_cwd); // TODO: make this conditional in the future @@ -66,984 +45,8 @@ fn main() -> Result<()> { // End ctrl-c protection section if let Some(path) = std::env::args().nth(1) { - // First, set up env vars as strings only - gather_parent_env_vars(&mut engine_state); - - let file = std::fs::read(&path).into_diagnostic()?; - - let (block, delta) = { - let mut working_set = StateWorkingSet::new(&engine_state); - trace!("parsing file: {}", path); - - let (output, err) = parse(&mut working_set, Some(&path), &file, false); - if let Some(err) = err { - report_error(&working_set, &err); - - std::process::exit(1); - } - (output, working_set.render()) - }; - - if let Err(err) = engine_state.merge_delta(delta, None, &init_cwd) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &err); - } - - let mut stack = nu_protocol::engine::Stack::new(); - - // Set up our initial config to start from - stack.vars.insert( - CONFIG_VARIABLE_ID, - Value::Record { - cols: vec![], - vals: vec![], - span: Span { start: 0, end: 0 }, - }, - ); - - let config = match stack.get_config() { - Ok(config) => config, - Err(e) => { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &e); - Config::default() - } - }; - - // Merge the delta in case env vars changed in the config - match nu_engine::env::current_dir(&engine_state, &stack) { - Ok(cwd) => { - if let Err(e) = engine_state.merge_delta(StateDelta::new(), Some(&mut stack), cwd) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - } - } - Err(e) => { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - } - } - - // Translate environment variables from Strings to Values - if let Some(e) = convert_env_values(&mut engine_state, &stack, &config) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - std::process::exit(1); - } - - match eval_block( - &engine_state, - &mut stack, - &block, - PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored - ) { - Ok(pipeline_data) => { - for item in pipeline_data { - if let Value::Error { error } = item { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &error); - - std::process::exit(1); - } - println!("{}", item.into_string("\n", &config)); - } - - // Next, let's check if there are any flags we want to pass to the main function - let args: Vec = std::env::args().skip(2).collect(); - - if args.is_empty() && engine_state.find_decl(b"main").is_none() { - return Ok(()); - } - - let args = format!("main {}", args.join(" ")).as_bytes().to_vec(); - - let (block, delta) = { - let mut working_set = StateWorkingSet::new(&engine_state); - let (output, err) = parse(&mut working_set, Some(""), &args, false); - if let Some(err) = err { - report_error(&working_set, &err); - - std::process::exit(1); - } - (output, working_set.render()) - }; - - let cwd = nu_engine::env::current_dir_str(&engine_state, &stack)?; - - if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &err); - } - - match eval_block( - &engine_state, - &mut stack, - &block, - PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored - ) { - Ok(pipeline_data) => { - for item in pipeline_data { - if let Value::Error { error } = item { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &error); - - std::process::exit(1); - } - println!("{}", item.into_string("\n", &config)); - } - } - Err(err) => { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &err); - - std::process::exit(1); - } - } - } - Err(err) => { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &err); - - std::process::exit(1); - } - } - - Ok(()) + eval_file::evaluate(path, init_cwd, &mut engine_state) } else { - use reedline::{FileBackedHistory, Reedline, Signal}; - - let mut entry_num = 0; - - let mut nu_prompt = NushellPrompt::new(); - let mut stack = nu_protocol::engine::Stack::new(); - - // First, set up env vars as strings only - gather_parent_env_vars(&mut engine_state); - - // Set up our initial config to start from - stack.vars.insert( - CONFIG_VARIABLE_ID, - Value::Record { - cols: vec![], - vals: vec![], - span: Span::new(0, 0), - }, - ); - - #[cfg(feature = "plugin")] - { - // Reading signatures from signature file - // The plugin.nu file stores the parsed signature collected from each registered plugin - if let Some(mut plugin_path) = nu_path::config_dir() { - // Path to store plugins signatures - plugin_path.push("nushell"); - plugin_path.push("plugin.nu"); - engine_state.plugin_signatures = Some(plugin_path.clone()); - - let plugin_filename = plugin_path.to_string_lossy().to_owned(); - - if let Ok(contents) = std::fs::read_to_string(&plugin_path) { - eval_source(&mut engine_state, &mut stack, &contents, &plugin_filename); - } - } - } - - // Load config startup file - if let Some(mut config_path) = nu_path::config_dir() { - config_path.push("nushell"); - - // Create config directory if it does not exist - if !config_path.exists() { - if let Err(err) = std::fs::create_dir_all(&config_path) { - eprintln!("Failed to create config directory: {}", err); - } - } else { - config_path.push("config.nu"); - - if config_path.exists() { - // FIXME: remove this message when we're ready - println!("Loading config from: {:?}", config_path); - let config_filename = config_path.to_string_lossy().to_owned(); - - if let Ok(contents) = std::fs::read_to_string(&config_path) { - eval_source(&mut engine_state, &mut stack, &contents, &config_filename); - // Merge the delta in case env vars changed in the config - match nu_engine::env::current_dir(&engine_state, &stack) { - Ok(cwd) => { - if let Err(e) = engine_state.merge_delta( - StateDelta::new(), - Some(&mut stack), - cwd, - ) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - } - } - Err(e) => { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - } - } - } - } - } - } - - // Get the config - let config = match stack.get_config() { - Ok(config) => config, - Err(e) => { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &e); - Config::default() - } - }; - - use logger::{configure, logger}; - - logger(|builder| { - configure(&config.log_level, builder)?; - // trace_filters(self, builder)?; - // debug_filters(self, builder)?; - - Ok(()) - })?; - - // Translate environment variables from Strings to Values - if let Some(e) = convert_env_values(&mut engine_state, &stack, &config) { - let working_set = StateWorkingSet::new(&engine_state); - report_error(&working_set, &e); - } - - let history_path = nu_path::config_dir().and_then(|mut history_path| { - history_path.push("nushell"); - history_path.push("history.txt"); - - if !history_path.exists() { - // Creating an empty file to store the history - match std::fs::File::create(&history_path) { - Ok(_) => Some(history_path), - Err(_) => None, - } - } else { - Some(history_path) - } - }); - - loop { - let config = match stack.get_config() { - Ok(config) => config, - Err(e) => { - let working_set = StateWorkingSet::new(&engine_state); - - report_error(&working_set, &e); - Config::default() - } - }; - - //Reset the ctrl-c handler - ctrlc.store(false, Ordering::SeqCst); - - let mut keybindings = default_emacs_keybindings(); - keybindings.add_binding( - KeyModifiers::SHIFT, - KeyCode::BackTab, - ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![EditCommand::InsertChar('p')]), - ReedlineEvent::Enter, - ]), - ); - let edit_mode = Box::new(Emacs::new(keybindings)); - - let line_editor = Reedline::create() - .into_diagnostic()? - .with_highlighter(Box::new(NuHighlighter { - engine_state: engine_state.clone(), - config: config.clone(), - })) - .with_animation(config.animate_prompt) - .with_validator(Box::new(NuValidator { - engine_state: engine_state.clone(), - })) - .with_edit_mode(edit_mode) - .with_ansi_colors(config.use_ansi_coloring) - .with_menu_completer( - Box::new(NuCompleter::new(engine_state.clone())), - create_menu_input(&config), - ); - - //FIXME: if config.use_ansi_coloring is false then we should - // turn off the hinter but I don't see any way to do that yet. - - let color_hm = get_color_config(&config); - - let line_editor = if let Some(history_path) = history_path.clone() { - let history = std::fs::read_to_string(&history_path); - if history.is_ok() { - line_editor - .with_hinter(Box::new( - DefaultHinter::default() - .with_history() - .with_style(color_hm["hints"]), - )) - .with_history(Box::new( - FileBackedHistory::with_file( - config.max_history_size as usize, - history_path.clone(), - ) - .into_diagnostic()?, - )) - .into_diagnostic()? - } else { - line_editor - } - } else { - line_editor - }; - - // The line editor default mode is emacs mode. For the moment we only - // need to check for vi mode - let mut line_editor = if config.edit_mode == "vi" { - let edit_mode = Box::new(Vi::default()); - line_editor.with_edit_mode(edit_mode) - } else { - line_editor - }; - - let prompt = update_prompt(&config, &engine_state, &stack, &mut nu_prompt); - - entry_num += 1; - - let input = line_editor.read_line(prompt); - match input { - Ok(Signal::Success(s)) => { - let tokens = lex(s.as_bytes(), 0, &[], &[], false); - // Check if this is a single call to a directory, if so auto-cd - let cwd = nu_engine::env::current_dir_str(&engine_state, &stack)?; - let path = nu_path::expand_path_with(&s, &cwd); - - let orig = s.clone(); - - if (orig.starts_with('.') - || orig.starts_with('~') - || orig.starts_with('/') - || orig.starts_with('\\')) - && path.is_dir() - && tokens.0.len() == 1 - { - // We have an auto-cd - let (path, span) = { - if !path.exists() { - let working_set = StateWorkingSet::new(&engine_state); - - report_error( - &working_set, - &ShellError::DirectoryNotFound(tokens.0[0].span), - ); - } - - let path = nu_path::canonicalize_with(path, &cwd) - .expect("internal error: cannot canonicalize known path"); - (path.to_string_lossy().to_string(), tokens.0[0].span) - }; - - //FIXME: this only changes the current scope, but instead this environment variable - //should probably be a block that loads the information from the state in the overlay - stack.add_env_var( - "PWD".into(), - Value::String { - val: path.clone(), - span: Span { start: 0, end: 0 }, - }, - ); - let cwd = Value::String { val: cwd, span }; - - let shells = stack.get_env_var(&engine_state, "NUSHELL_SHELLS"); - let mut shells = if let Some(v) = shells { - v.as_list() - .map(|x| x.to_vec()) - .unwrap_or_else(|_| vec![cwd]) - } else { - vec![cwd] - }; - - let current_shell = - stack.get_env_var(&engine_state, "NUSHELL_CURRENT_SHELL"); - let current_shell = if let Some(v) = current_shell { - v.as_integer().unwrap_or_default() as usize - } else { - 0 - }; - - shells[current_shell] = Value::String { val: path, span }; - - stack.add_env_var( - "NUSHELL_SHELLS".into(), - Value::List { vals: shells, span }, - ); - } else { - trace!("eval source: {}", s); - - eval_source( - &mut engine_state, - &mut stack, - &s, - &format!("entry #{}", entry_num), - ); - } - // FIXME: permanent state changes like this hopefully in time can be removed - // and be replaced by just passing the cwd in where needed - if let Some(cwd) = stack.get_env_var(&engine_state, "PWD") { - let path = cwd.as_string()?; - let _ = std::env::set_current_dir(path); - engine_state.env_vars.insert("PWD".into(), cwd); - } - } - Ok(Signal::CtrlC) => { - // `Reedline` clears the line content. New prompt is shown - } - Ok(Signal::CtrlD) => { - // When exiting clear to a new line - println!(); - break; - } - Ok(Signal::CtrlL) => { - line_editor.clear_screen().into_diagnostic()?; - } - Err(err) => { - let message = err.to_string(); - if !message.contains("duration") { - println!("Error: {:?}", err); - } - } - } - } - - Ok(()) - } -} - -// This creates an input object for the context menu based on the dictionary -// stored in the config variable -fn create_menu_input(config: &Config) -> ContextMenuInput { - let mut input = ContextMenuInput::default(); - - input = match config - .menu_config - .get("columns") - .and_then(|value| value.as_integer().ok()) - { - Some(value) => input.with_columns(value as u16), - None => input, - }; - - input = input.with_col_width( - config - .menu_config - .get("col_width") - .and_then(|value| value.as_integer().ok()) - .map(|value| value as usize), - ); - - input = match config - .menu_config - .get("col_padding") - .and_then(|value| value.as_integer().ok()) - { - Some(value) => input.with_col_padding(value as usize), - None => input, - }; - - input = match config - .menu_config - .get("text_style") - .and_then(|value| value.as_string().ok()) - { - Some(value) => input.with_text_style(lookup_ansi_color_style(&value)), - None => input, - }; - - input = match config - .menu_config - .get("selected_text_style") - .and_then(|value| value.as_string().ok()) - { - Some(value) => input.with_selected_text_style(lookup_ansi_color_style(&value)), - None => input, - }; - - input -} - -// This fill collect environment variables from std::env and adds them to a stack. -// -// In order to ensure the values have spans, it first creates a dummy file, writes the collected -// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env' -// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done. -fn gather_parent_env_vars(engine_state: &mut EngineState) { - // Some helper functions - fn get_surround_char(s: &str) -> Option { - if s.contains('"') { - if s.contains('\'') { - None - } else { - Some('\'') - } - } else { - Some('"') - } - } - - fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) { - let working_set = StateWorkingSet::new(engine_state); - report_error( - &working_set, - &ShellError::LabeledError( - format!("Environment variable was not captured: {}", env_str), - msg.into(), - ), - ); - } - - fn put_env_to_fake_file( - name: &str, - val: &str, - fake_env_file: &mut String, - engine_state: &EngineState, - ) { - let (c_name, c_val) = - if let (Some(cn), Some(cv)) = (get_surround_char(name), get_surround_char(val)) { - (cn, cv) - } else { - // environment variable with its name or value containing both ' and " is ignored - report_capture_error( - engine_state, - &format!("{}={}", name, val), - "Name or value should not contain both ' and \" at the same time.", - ); - return; - }; - - fake_env_file.push(c_name); - fake_env_file.push_str(name); - fake_env_file.push(c_name); - fake_env_file.push('='); - fake_env_file.push(c_val); - fake_env_file.push_str(val); - fake_env_file.push(c_val); - fake_env_file.push('\n'); - } - - let mut fake_env_file = String::new(); - - // Make sure we always have PWD - if std::env::var("PWD").is_err() { - match std::env::current_dir() { - Ok(cwd) => { - put_env_to_fake_file( - "PWD", - &cwd.to_string_lossy(), - &mut fake_env_file, - engine_state, - ); - } - Err(e) => { - // Could not capture current working directory - let working_set = StateWorkingSet::new(engine_state); - report_error( - &working_set, - &ShellError::LabeledError( - "Current directory not found".to_string(), - format!("Retrieving current directory failed: {:?}", e), - ), - ); - } - } - } - - // Write all the env vars into a fake file - for (name, val) in std::env::vars() { - put_env_to_fake_file(&name, &val, &mut fake_env_file, engine_state); - } - - // Lex the fake file, assign spans to all environment variables and add them - // to stack - let span_offset = engine_state.next_span_start(); - - engine_state.add_file( - "Host Environment Variables".to_string(), - fake_env_file.as_bytes().to_vec(), - ); - - let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true); - - for token in tokens { - if let Token { - contents: TokenContents::Item, - span: full_span, - } = token - { - let contents = engine_state.get_span_contents(&full_span); - let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true); - - let name = if let Some(Token { - contents: TokenContents::Item, - span, - }) = parts.get(0) - { - let bytes = engine_state.get_span_contents(span); - - if bytes.len() < 2 { - report_capture_error( - engine_state, - &String::from_utf8_lossy(contents), - "Got empty name.", - ); - - continue; - } - - let bytes = trim_quotes(bytes); - String::from_utf8_lossy(bytes).to_string() - } else { - report_capture_error( - engine_state, - &String::from_utf8_lossy(contents), - "Got empty name.", - ); - - continue; - }; - - let value = if let Some(Token { - contents: TokenContents::Item, - span, - }) = parts.get(2) - { - let bytes = engine_state.get_span_contents(span); - - if bytes.len() < 2 { - report_capture_error( - engine_state, - &String::from_utf8_lossy(contents), - "Got empty value.", - ); - - continue; - } - - let bytes = trim_quotes(bytes); - - Value::String { - val: String::from_utf8_lossy(bytes).to_string(), - span: *span, - } - } else { - report_capture_error( - engine_state, - &String::from_utf8_lossy(contents), - "Got empty value.", - ); - - continue; - }; - - // stack.add_env_var(name, value); - engine_state.env_vars.insert(name, value); - } - } -} - -fn print_pipeline_data( - input: PipelineData, - engine_state: &EngineState, - stack: &mut Stack, -) -> Result<(), ShellError> { - // If the table function is in the declarations, then we can use it - // to create the table value that will be printed in the terminal - - let config = stack.get_config().unwrap_or_default(); - - match input { - PipelineData::StringStream(stream, _, _) => { - for s in stream { - print!("{}", s?); - let _ = std::io::stdout().flush(); - } - return Ok(()); - } - PipelineData::ByteStream(stream, _, _) => { - let mut address_offset = 0; - for v in stream { - let cfg = nu_pretty_hex::HexConfig { - title: false, - address_offset, - ..Default::default() - }; - - let v = v?; - address_offset += v.len(); - - let s = if v.iter().all(|x| x.is_ascii()) { - format!("{}", String::from_utf8_lossy(&v)) - } else { - nu_pretty_hex::config_hex(&v, cfg) - }; - println!("{}", s); - } - return Ok(()); - } - _ => {} - } - - match engine_state.find_decl("table".as_bytes()) { - Some(decl_id) => { - let table = - engine_state - .get_decl(decl_id) - .run(engine_state, stack, &Call::new(), input)?; - - for item in table { - let stdout = std::io::stdout(); - - if let Value::Error { error } = item { - return Err(error); - } - - let mut out = item.into_string("\n", &config); - out.push('\n'); - - match stdout.lock().write_all(out.as_bytes()) { - Ok(_) => (), - Err(err) => eprintln!("{}", err), - }; - } - } - None => { - for item in input { - let stdout = std::io::stdout(); - - if let Value::Error { error } = item { - return Err(error); - } - - let mut out = item.into_string("\n", &config); - out.push('\n'); - - match stdout.lock().write_all(out.as_bytes()) { - Ok(_) => (), - Err(err) => eprintln!("{}", err), - }; - } - } - }; - - Ok(()) -} - -fn get_prompt_indicators( - config: &Config, - engine_state: &EngineState, - stack: &Stack, -) -> (String, String, String, String) { - let prompt_indicator = match stack.get_env_var(engine_state, PROMPT_INDICATOR) { - Some(pi) => pi.into_string("", config), - None => "〉".to_string(), - }; - - let prompt_vi_insert = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_INSERT) { - Some(pvii) => pvii.into_string("", config), - None => ": ".to_string(), - }; - - let prompt_vi_visual = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_VISUAL) { - Some(pviv) => pviv.into_string("", config), - None => "v ".to_string(), - }; - - let prompt_multiline = match stack.get_env_var(engine_state, PROMPT_MULTILINE_INDICATOR) { - Some(pm) => pm.into_string("", config), - None => "::: ".to_string(), - }; - - ( - prompt_indicator, - prompt_vi_insert, - prompt_vi_visual, - prompt_multiline, - ) -} - -fn update_prompt<'prompt>( - config: &Config, - engine_state: &EngineState, - stack: &Stack, - nu_prompt: &'prompt mut NushellPrompt, -) -> &'prompt dyn Prompt { - // get the other indicators - let ( - prompt_indicator_string, - prompt_vi_insert_string, - prompt_vi_visual_string, - prompt_multiline_string, - ) = get_prompt_indicators(config, engine_state, stack); - - let mut stack = stack.clone(); - - // apply the other indicators - nu_prompt.update_all_prompt_strings( - get_prompt_string(PROMPT_COMMAND, config, engine_state, &mut stack), - get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, &mut stack), - prompt_indicator_string, - prompt_vi_insert_string, - prompt_vi_visual_string, - prompt_multiline_string, - ); - - nu_prompt as &dyn Prompt -} - -fn get_prompt_string( - prompt: &str, - config: &Config, - engine_state: &EngineState, - stack: &mut Stack, -) -> Option { - stack - .get_env_var(engine_state, prompt) - .and_then(|v| v.as_block().ok()) - .and_then(|block_id| { - let block = engine_state.get_block(block_id); - eval_block( - engine_state, - stack, - block, - PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored - ) - .ok() - }) - .and_then(|pipeline_data| pipeline_data.collect_string("", config).ok()) -} - -fn eval_source( - engine_state: &mut EngineState, - stack: &mut Stack, - source: &str, - fname: &str, -) -> bool { - trace!("eval_source"); - - let (block, delta) = { - let mut working_set = StateWorkingSet::new(engine_state); - let (output, err) = parse( - &mut working_set, - Some(fname), // format!("entry #{}", entry_num) - source.as_bytes(), - false, - ); - if let Some(err) = err { - report_error(&working_set, &err); - return false; - } - - (output, working_set.render()) - }; - - let cwd = match nu_engine::env::current_dir_str(engine_state, stack) { - Ok(p) => PathBuf::from(p), - Err(e) => { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &e); - get_init_cwd() - } - }; - - if let Err(err) = engine_state.merge_delta(delta, Some(stack), &cwd) { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &err); - } - - match eval_block( - engine_state, - stack, - &block, - PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored - ) { - Ok(pipeline_data) => { - if let Err(err) = print_pipeline_data(pipeline_data, engine_state, stack) { - let working_set = StateWorkingSet::new(engine_state); - - report_error(&working_set, &err); - - return false; - } - - // reset vt processing, aka ansi because illbehaved externals can break it - #[cfg(windows)] - { - let _ = enable_vt_processing(); - } - } - Err(err) => { - let working_set = StateWorkingSet::new(engine_state); - - report_error(&working_set, &err); - - return false; - } - } - - true -} - -#[cfg(windows)] -pub fn enable_vt_processing() -> Result<(), ShellError> { - pub const ENABLE_PROCESSED_OUTPUT: u32 = 0x0001; - pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004; - // let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING; - - let console_mode = ConsoleMode::from(Handle::current_out_handle()?); - let old_mode = console_mode.mode()?; - - // researching odd ansi behavior in windows terminal repo revealed that - // enable_processed_output and enable_virtual_terminal_processing should be used - // also, instead of checking old_mode & mask, just set the mode already - - // if old_mode & mask == 0 { - console_mode - .set_mode(old_mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)?; - // } - - Ok(()) -} - -pub fn report_error( - working_set: &StateWorkingSet, - error: &(dyn miette::Diagnostic + Send + Sync + 'static), -) { - eprintln!("Error: {:?}", CliError(error, working_set)); - // reset vt processing, aka ansi because illbehaved externals can break it - #[cfg(windows)] - { - let _ = enable_vt_processing(); - } -} - -fn get_init_cwd() -> PathBuf { - match std::env::current_dir() { - Ok(cwd) => cwd, - Err(_) => match std::env::var("PWD".to_string()) { - Ok(cwd) => PathBuf::from(cwd), - Err(_) => match nu_path::home_dir() { - Some(cwd) => cwd, - None => PathBuf::new(), - }, - }, + repl::evaluate(ctrlc, &mut engine_state) } } diff --git a/src/prompt_update.rs b/src/prompt_update.rs new file mode 100644 index 0000000000..42c2d5a6fc --- /dev/null +++ b/src/prompt_update.rs @@ -0,0 +1,99 @@ +use nu_cli::NushellPrompt; +use nu_engine::eval_block; +use nu_protocol::{ + engine::{EngineState, Stack}, + Config, PipelineData, Span, +}; +use reedline::Prompt; + +// Name of environment variable where the prompt could be stored +pub(crate) const PROMPT_COMMAND: &str = "PROMPT_COMMAND"; +pub(crate) const PROMPT_COMMAND_RIGHT: &str = "PROMPT_COMMAND_RIGHT"; +pub(crate) const PROMPT_INDICATOR: &str = "PROMPT_INDICATOR"; +pub(crate) const PROMPT_INDICATOR_VI_INSERT: &str = "PROMPT_INDICATOR_VI_INSERT"; +pub(crate) const PROMPT_INDICATOR_VI_VISUAL: &str = "PROMPT_INDICATOR_VI_VISUAL"; +pub(crate) const PROMPT_MULTILINE_INDICATOR: &str = "PROMPT_MULTILINE_INDICATOR"; + +pub(crate) fn get_prompt_indicators( + config: &Config, + engine_state: &EngineState, + stack: &Stack, +) -> (String, String, String, String) { + let prompt_indicator = match stack.get_env_var(engine_state, PROMPT_INDICATOR) { + Some(pi) => pi.into_string("", config), + None => "〉".to_string(), + }; + + let prompt_vi_insert = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_INSERT) { + Some(pvii) => pvii.into_string("", config), + None => ": ".to_string(), + }; + + let prompt_vi_visual = match stack.get_env_var(engine_state, PROMPT_INDICATOR_VI_VISUAL) { + Some(pviv) => pviv.into_string("", config), + None => "v ".to_string(), + }; + + let prompt_multiline = match stack.get_env_var(engine_state, PROMPT_MULTILINE_INDICATOR) { + Some(pm) => pm.into_string("", config), + None => "::: ".to_string(), + }; + + ( + prompt_indicator, + prompt_vi_insert, + prompt_vi_visual, + prompt_multiline, + ) +} + +fn get_prompt_string( + prompt: &str, + config: &Config, + engine_state: &EngineState, + stack: &mut Stack, +) -> Option { + stack + .get_env_var(engine_state, prompt) + .and_then(|v| v.as_block().ok()) + .and_then(|block_id| { + let block = engine_state.get_block(block_id); + eval_block( + engine_state, + stack, + block, + PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored + ) + .ok() + }) + .and_then(|pipeline_data| pipeline_data.collect_string("", config).ok()) +} + +pub(crate) fn update_prompt<'prompt>( + config: &Config, + engine_state: &EngineState, + stack: &Stack, + nu_prompt: &'prompt mut NushellPrompt, +) -> &'prompt dyn Prompt { + // get the other indicators + let ( + prompt_indicator_string, + prompt_vi_insert_string, + prompt_vi_visual_string, + prompt_multiline_string, + ) = get_prompt_indicators(config, engine_state, stack); + + let mut stack = stack.clone(); + + // apply the other indicators + nu_prompt.update_all_prompt_strings( + get_prompt_string(PROMPT_COMMAND, config, engine_state, &mut stack), + get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, &mut stack), + prompt_indicator_string, + prompt_vi_insert_string, + prompt_vi_visual_string, + prompt_multiline_string, + ); + + nu_prompt as &dyn Prompt +} diff --git a/src/reedline_config.rs b/src/reedline_config.rs new file mode 100644 index 0000000000..85dba64099 --- /dev/null +++ b/src/reedline_config.rs @@ -0,0 +1,104 @@ +use crossterm::event::{KeyCode, KeyModifiers}; +use nu_color_config::lookup_ansi_color_style; +use nu_protocol::{Config, EventType, ParsedKeybinding, ShellError}; +use reedline::{ + default_emacs_keybindings, ContextMenuInput, EditCommand, Keybindings, ReedlineEvent, +}; + +// This creates an input object for the context menu based on the dictionary +// stored in the config variable +pub(crate) fn create_menu_input(config: &Config) -> ContextMenuInput { + let mut input = ContextMenuInput::default(); + + input = match config + .menu_config + .get("columns") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => input.with_columns(value as u16), + None => input, + }; + + input = input.with_col_width( + config + .menu_config + .get("col_width") + .and_then(|value| value.as_integer().ok()) + .map(|value| value as usize), + ); + + input = match config + .menu_config + .get("col_padding") + .and_then(|value| value.as_integer().ok()) + { + Some(value) => input.with_col_padding(value as usize), + None => input, + }; + + input = match config + .menu_config + .get("text_style") + .and_then(|value| value.as_string().ok()) + { + Some(value) => input.with_text_style(lookup_ansi_color_style(&value)), + None => input, + }; + + input = match config + .menu_config + .get("selected_text_style") + .and_then(|value| value.as_string().ok()) + { + Some(value) => input.with_selected_text_style(lookup_ansi_color_style(&value)), + None => input, + }; + + input +} + +pub(crate) fn create_keybindings( + parsed_keybindings: &[ParsedKeybinding], +) -> Result { + let mut keybindings = default_emacs_keybindings(); + + // temporal keybinding with multiple events + keybindings.add_binding( + KeyModifiers::SHIFT, + KeyCode::BackTab, + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::InsertChar('p')]), + ReedlineEvent::Enter, + ]), + ); + + for keybinding in parsed_keybindings { + let modifier = match keybinding.modifier.as_str() { + "CONTROL" => KeyModifiers::CONTROL, + "SHIFT" => KeyModifiers::CONTROL, + _ => unimplemented!(), + }; + + let keycode = match keybinding.keycode.as_str() { + c if c.starts_with("Char_") => { + let char = c.replace("Char_", ""); + let char = char.chars().next().expect("correct"); + KeyCode::Char(char) + } + "down" => KeyCode::Down, + _ => unimplemented!(), + }; + + let event = match &keybinding.event { + EventType::Single(name) => match name.as_str() { + "Complete" => ReedlineEvent::Complete, + "ContextMenu" => ReedlineEvent::ContextMenu, + _ => unimplemented!(), + }, + }; + + keybindings.add_binding(modifier, keycode, event); + } + + Ok(keybindings) +} diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 0000000000..18280065af --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,257 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use crate::utils::{eval_source, gather_parent_env_vars, report_error}; +use crate::{config_files, prompt_update, reedline_config}; +use log::trace; +use miette::{IntoDiagnostic, Result}; +use nu_cli::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt}; +use nu_color_config::get_color_config; +use nu_engine::convert_env_values; +use nu_parser::lex; +use nu_protocol::{ + engine::{EngineState, StateWorkingSet}, + Config, ShellError, Span, Value, CONFIG_VARIABLE_ID, +}; +use reedline::{DefaultHinter, Emacs, Vi}; + +pub(crate) fn evaluate(ctrlc: Arc, engine_state: &mut EngineState) -> Result<()> { + use crate::logger::{configure, logger}; + use reedline::{FileBackedHistory, Reedline, Signal}; + + let mut entry_num = 0; + + let mut nu_prompt = NushellPrompt::new(); + let mut stack = nu_protocol::engine::Stack::new(); + + // First, set up env vars as strings only + gather_parent_env_vars(engine_state); + + // Set up our initial config to start from + stack.vars.insert( + CONFIG_VARIABLE_ID, + Value::Record { + cols: vec![], + vals: vec![], + span: Span::new(0, 0), + }, + ); + + #[cfg(feature = "plugin")] + config_files::read_plugin_file(engine_state, &mut stack); + + config_files::read_config_file(engine_state, &mut stack); + let history_path = config_files::create_history_path(); + + // Load config struct form config variable + let config = match stack.get_config() { + Ok(config) => config, + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &e); + Config::default() + } + }; + + logger(|builder| { + configure(&config.log_level, builder)?; + // trace_filters(self, builder)?; + // debug_filters(self, builder)?; + + Ok(()) + })?; + + // Translate environment variables from Strings to Values + if let Some(e) = convert_env_values(engine_state, &stack, &config) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + } + + loop { + let config = match stack.get_config() { + Ok(config) => config, + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &e); + Config::default() + } + }; + + //Reset the ctrl-c handler + ctrlc.store(false, Ordering::SeqCst); + + let keybindings = reedline_config::create_keybindings(&config.keybindings)?; + let edit_mode = Box::new(Emacs::new(keybindings)); + + let line_editor = Reedline::create() + .into_diagnostic()? + // .with_completion_action_handler(Box::new(fuzzy_completion::FuzzyCompletion { + // completer: Box::new(NuCompleter::new(engine_state.clone())), + // })) + // .with_completion_action_handler(Box::new( + // ListCompletionHandler::default().with_completer(Box::new(completer)), + // )) + .with_highlighter(Box::new(NuHighlighter { + engine_state: engine_state.clone(), + config: config.clone(), + })) + .with_animation(config.animate_prompt) + .with_validator(Box::new(NuValidator { + engine_state: engine_state.clone(), + })) + .with_edit_mode(edit_mode) + .with_ansi_colors(config.use_ansi_coloring) + .with_menu_completer( + Box::new(NuCompleter::new(engine_state.clone())), + reedline_config::create_menu_input(&config), + ); + + //FIXME: if config.use_ansi_coloring is false then we should + // turn off the hinter but I don't see any way to do that yet. + + let color_hm = get_color_config(&config); + + let line_editor = if let Some(history_path) = history_path.clone() { + let history = std::fs::read_to_string(&history_path); + if history.is_ok() { + line_editor + .with_hinter(Box::new( + DefaultHinter::default() + .with_history() + .with_style(color_hm["hints"]), + )) + .with_history(Box::new( + FileBackedHistory::with_file( + config.max_history_size as usize, + history_path.clone(), + ) + .into_diagnostic()?, + )) + .into_diagnostic()? + } else { + line_editor + } + } else { + line_editor + }; + + // The line editor default mode is emacs mode. For the moment we only + // need to check for vi mode + let mut line_editor = if config.edit_mode == "vi" { + let edit_mode = Box::new(Vi::default()); + line_editor.with_edit_mode(edit_mode) + } else { + line_editor + }; + + let prompt = prompt_update::update_prompt(&config, engine_state, &stack, &mut nu_prompt); + + entry_num += 1; + + let input = line_editor.read_line(prompt); + match input { + Ok(Signal::Success(s)) => { + let tokens = lex(s.as_bytes(), 0, &[], &[], false); + // Check if this is a single call to a directory, if so auto-cd + let cwd = nu_engine::env::current_dir_str(engine_state, &stack)?; + let path = nu_path::expand_path_with(&s, &cwd); + + let orig = s.clone(); + + if (orig.starts_with('.') + || orig.starts_with('~') + || orig.starts_with('/') + || orig.starts_with('\\')) + && path.is_dir() + && tokens.0.len() == 1 + { + // We have an auto-cd + let (path, span) = { + if !path.exists() { + let working_set = StateWorkingSet::new(engine_state); + + report_error( + &working_set, + &ShellError::DirectoryNotFound(tokens.0[0].span), + ); + } + + let path = nu_path::canonicalize_with(path, &cwd) + .expect("internal error: cannot canonicalize known path"); + (path.to_string_lossy().to_string(), tokens.0[0].span) + }; + + //FIXME: this only changes the current scope, but instead this environment variable + //should probably be a block that loads the information from the state in the overlay + stack.add_env_var( + "PWD".into(), + Value::String { + val: path.clone(), + span: Span { start: 0, end: 0 }, + }, + ); + let cwd = Value::String { val: cwd, span }; + + let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS"); + let mut shells = if let Some(v) = shells { + v.as_list() + .map(|x| x.to_vec()) + .unwrap_or_else(|_| vec![cwd]) + } else { + vec![cwd] + }; + + let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL"); + let current_shell = if let Some(v) = current_shell { + v.as_integer().unwrap_or_default() as usize + } else { + 0 + }; + + shells[current_shell] = Value::String { val: path, span }; + + stack.add_env_var("NUSHELL_SHELLS".into(), Value::List { vals: shells, span }); + } else { + trace!("eval source: {}", s); + + eval_source( + engine_state, + &mut stack, + &s, + &format!("entry #{}", entry_num), + ); + } + // FIXME: permanent state changes like this hopefully in time can be removed + // and be replaced by just passing the cwd in where needed + if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { + let path = cwd.as_string()?; + let _ = std::env::set_current_dir(path); + engine_state.env_vars.insert("PWD".into(), cwd); + } + } + Ok(Signal::CtrlC) => { + // `Reedline` clears the line content. New prompt is shown + } + Ok(Signal::CtrlD) => { + // When exiting clear to a new line + println!(); + break; + } + Ok(Signal::CtrlL) => { + line_editor.clear_screen().into_diagnostic()?; + } + Err(err) => { + let message = err.to_string(); + if !message.contains("duration") { + println!("Error: {:?}", err); + } + } + } + } + + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000..79e5c8441c --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,395 @@ +use log::trace; +use nu_cli::CliError; +use nu_engine::eval_block; +use nu_parser::{lex, parse, trim_quotes, Token, TokenContents}; +use nu_protocol::{ + ast::Call, + engine::{EngineState, Stack, StateWorkingSet}, + PipelineData, ShellError, Span, Value, +}; +use std::{io::Write, path::PathBuf}; + +// This fill collect environment variables from std::env and adds them to a stack. +// +// In order to ensure the values have spans, it first creates a dummy file, writes the collected +// env vars into it (in a "NAME"="value" format, quite similar to the output of the Unix 'env' +// tool), then uses the file to get the spans. The file stays in memory, no filesystem IO is done. +pub(crate) fn gather_parent_env_vars(engine_state: &mut EngineState) { + // Some helper functions + fn get_surround_char(s: &str) -> Option { + if s.contains('"') { + if s.contains('\'') { + None + } else { + Some('\'') + } + } else { + Some('"') + } + } + + fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) { + let working_set = StateWorkingSet::new(engine_state); + report_error( + &working_set, + &ShellError::LabeledError( + format!("Environment variable was not captured: {}", env_str), + msg.into(), + ), + ); + } + + fn put_env_to_fake_file( + name: &str, + val: &str, + fake_env_file: &mut String, + engine_state: &EngineState, + ) { + let (c_name, c_val) = + if let (Some(cn), Some(cv)) = (get_surround_char(name), get_surround_char(val)) { + (cn, cv) + } else { + // environment variable with its name or value containing both ' and " is ignored + report_capture_error( + engine_state, + &format!("{}={}", name, val), + "Name or value should not contain both ' and \" at the same time.", + ); + return; + }; + + fake_env_file.push(c_name); + fake_env_file.push_str(name); + fake_env_file.push(c_name); + fake_env_file.push('='); + fake_env_file.push(c_val); + fake_env_file.push_str(val); + fake_env_file.push(c_val); + fake_env_file.push('\n'); + } + + let mut fake_env_file = String::new(); + + // Make sure we always have PWD + if std::env::var("PWD").is_err() { + match std::env::current_dir() { + Ok(cwd) => { + put_env_to_fake_file( + "PWD", + &cwd.to_string_lossy(), + &mut fake_env_file, + engine_state, + ); + } + Err(e) => { + // Could not capture current working directory + let working_set = StateWorkingSet::new(engine_state); + report_error( + &working_set, + &ShellError::LabeledError( + "Current directory not found".to_string(), + format!("Retrieving current directory failed: {:?}", e), + ), + ); + } + } + } + + // Write all the env vars into a fake file + for (name, val) in std::env::vars() { + put_env_to_fake_file(&name, &val, &mut fake_env_file, engine_state); + } + + // Lex the fake file, assign spans to all environment variables and add them + // to stack + let span_offset = engine_state.next_span_start(); + + engine_state.add_file( + "Host Environment Variables".to_string(), + fake_env_file.as_bytes().to_vec(), + ); + + let (tokens, _) = lex(fake_env_file.as_bytes(), span_offset, &[], &[], true); + + for token in tokens { + if let Token { + contents: TokenContents::Item, + span: full_span, + } = token + { + let contents = engine_state.get_span_contents(&full_span); + let (parts, _) = lex(contents, full_span.start, &[], &[b'='], true); + + let name = if let Some(Token { + contents: TokenContents::Item, + span, + }) = parts.get(0) + { + let bytes = engine_state.get_span_contents(span); + + if bytes.len() < 2 { + report_capture_error( + engine_state, + &String::from_utf8_lossy(contents), + "Got empty name.", + ); + + continue; + } + + let bytes = trim_quotes(bytes); + String::from_utf8_lossy(bytes).to_string() + } else { + report_capture_error( + engine_state, + &String::from_utf8_lossy(contents), + "Got empty name.", + ); + + continue; + }; + + let value = if let Some(Token { + contents: TokenContents::Item, + span, + }) = parts.get(2) + { + let bytes = engine_state.get_span_contents(span); + + if bytes.len() < 2 { + report_capture_error( + engine_state, + &String::from_utf8_lossy(contents), + "Got empty value.", + ); + + continue; + } + + let bytes = trim_quotes(bytes); + + Value::String { + val: String::from_utf8_lossy(bytes).to_string(), + span: *span, + } + } else { + report_capture_error( + engine_state, + &String::from_utf8_lossy(contents), + "Got empty value.", + ); + + continue; + }; + + // stack.add_env_var(name, value); + engine_state.env_vars.insert(name, value); + } + } +} + +fn print_pipeline_data( + input: PipelineData, + engine_state: &EngineState, + stack: &mut Stack, +) -> Result<(), ShellError> { + // If the table function is in the declarations, then we can use it + // to create the table value that will be printed in the terminal + + let config = stack.get_config().unwrap_or_default(); + + match input { + PipelineData::StringStream(stream, _, _) => { + for s in stream { + print!("{}", s?); + let _ = std::io::stdout().flush(); + } + return Ok(()); + } + PipelineData::ByteStream(stream, _, _) => { + let mut address_offset = 0; + for v in stream { + let cfg = nu_pretty_hex::HexConfig { + title: false, + address_offset, + ..Default::default() + }; + + let v = v?; + address_offset += v.len(); + + let s = if v.iter().all(|x| x.is_ascii()) { + format!("{}", String::from_utf8_lossy(&v)) + } else { + nu_pretty_hex::config_hex(&v, cfg) + }; + println!("{}", s); + } + return Ok(()); + } + _ => {} + } + + match engine_state.find_decl("table".as_bytes()) { + Some(decl_id) => { + let table = + engine_state + .get_decl(decl_id) + .run(engine_state, stack, &Call::new(), input)?; + + for item in table { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + None => { + for item in input { + let stdout = std::io::stdout(); + + if let Value::Error { error } = item { + return Err(error); + } + + let mut out = item.into_string("\n", &config); + out.push('\n'); + + match stdout.lock().write_all(out.as_bytes()) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + }; + } + } + }; + + Ok(()) +} + +pub(crate) fn eval_source( + engine_state: &mut EngineState, + stack: &mut Stack, + source: &str, + fname: &str, +) -> bool { + trace!("eval_source"); + + let (block, delta) = { + let mut working_set = StateWorkingSet::new(engine_state); + let (output, err) = parse( + &mut working_set, + Some(fname), // format!("entry #{}", entry_num) + source.as_bytes(), + false, + ); + if let Some(err) = err { + report_error(&working_set, &err); + return false; + } + + (output, working_set.render()) + }; + + let cwd = match nu_engine::env::current_dir_str(engine_state, stack) { + Ok(p) => PathBuf::from(p), + Err(e) => { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + get_init_cwd() + } + }; + + if let Err(err) = engine_state.merge_delta(delta, Some(stack), &cwd) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } + + match eval_block( + engine_state, + stack, + &block, + PipelineData::new(Span::new(0, 0)), // Don't try this at home, 0 span is ignored + ) { + Ok(pipeline_data) => { + if let Err(err) = print_pipeline_data(pipeline_data, engine_state, stack) { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &err); + + return false; + } + + // reset vt processing, aka ansi because illbehaved externals can break it + #[cfg(windows)] + { + let _ = enable_vt_processing(); + } + } + Err(err) => { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, &err); + + return false; + } + } + + true +} + +#[cfg(windows)] +pub fn enable_vt_processing() -> Result<(), ShellError> { + use crossterm_winapi::{ConsoleMode, Handle}; + + pub const ENABLE_PROCESSED_OUTPUT: u32 = 0x0001; + pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004; + // let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + let console_mode = ConsoleMode::from(Handle::current_out_handle()?); + let old_mode = console_mode.mode()?; + + // researching odd ansi behavior in windows terminal repo revealed that + // enable_processed_output and enable_virtual_terminal_processing should be used + // also, instead of checking old_mode & mask, just set the mode already + + // if old_mode & mask == 0 { + console_mode + .set_mode(old_mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)?; + // } + + Ok(()) +} + +pub fn report_error( + working_set: &StateWorkingSet, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + eprintln!("Error: {:?}", CliError(error, working_set)); + // reset vt processing, aka ansi because illbehaved externals can break it + #[cfg(windows)] + { + let _ = enable_vt_processing(); + } +} + +pub(crate) fn get_init_cwd() -> PathBuf { + match std::env::current_dir() { + Ok(cwd) => cwd, + Err(_) => match std::env::var("PWD".to_string()) { + Ok(cwd) => PathBuf::from(cwd), + Err(_) => match nu_path::home_dir() { + Some(cwd) => cwd, + None => PathBuf::new(), + }, + }, + } +}