Fix editor config for reedline and config nu/env (#10535)

# Description
This merges @horasal 's changes from #10246 and #10269

Closes #10205
Closes #8714

Fixes the bug that editor paths with spaces are unusable

Closes #10210 
Closes #10269


# User-Facing Changes
You can now either pass a string with the name of the executable or a
list with the executable and any flags to
`$env.config.buffer_editor`/`$env.EDITOR`/`$env.VISUAL`

Both the external buffer editor of reedline (by default bound to
`Ctrl-o`) and the commands `config nu` and `config env` will respect
those variables in the following order:
1. `$env.config.buffer_editor`
2. `$env.EDITOR`
3. `$env.VISUAL`

Example:
```
$env.EDITOR = "nvim"                      # The system-wide EDITOR is neovim
$env.config.buffer_editor = ["vim" "-p2"] # Force vim to open two tabs (not particularly useful)
$env.config.buffer_editor = null          # Unset `buffer_editor` -> Uses `$env.EDITOR` ergo nvim
```
# Tests + Formatting
None

---------

Co-authored-by: Horasal <1991933+horasal@users.noreply.github.com>
This commit is contained in:
Stefan Holderbach 2023-09-29 16:36:03 +02:00 committed by GitHub
parent f2af12af2c
commit 80a183dde2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 71 deletions

8
Cargo.lock generated
View File

@ -2746,6 +2746,7 @@ dependencies = [
"rstest", "rstest",
"sysinfo", "sysinfo",
"unicode-segmentation", "unicode-segmentation",
"uuid",
] ]
[[package]] [[package]]
@ -4291,8 +4292,7 @@ dependencies = [
[[package]] [[package]]
name = "reedline" name = "reedline"
version = "0.24.0" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/nushell/reedline.git?branch=main#b1344f6a653b7c37154a1dd79b3c4e34afe6cfc9"
checksum = "971ac45071721aae18927f3feb7e4c2b95cce387d96af185c5103166d332e55c"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossterm 0.27.0", "crossterm 0.27.0",
@ -5705,9 +5705,9 @@ checksum = "37db35583cf0ad896592892034f281ef0906c893b3b6d901acf3918357f28199"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.4.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]

View File

@ -164,7 +164,7 @@ bench = false
# To use a development version of a dependency please use a global override here # To use a development version of a dependency please use a global override here
# changing versions in each sub-crate of the workspace is tedious # changing versions in each sub-crate of the workspace is tedious
[patch.crates-io] [patch.crates-io]
# reedline = { git = "https://github.com/nushell/reedline.git", branch = "main"} reedline = { git = "https://github.com/nushell/reedline.git", branch = "main"}
# nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"}
# Criterion benchmarking setup # Criterion benchmarking setup

View File

@ -38,6 +38,7 @@ once_cell = "1.18"
percent-encoding = "2" percent-encoding = "2"
sysinfo = "0.29" sysinfo = "0.29"
unicode-segmentation = "1.10" unicode-segmentation = "1.10"
uuid = { version = "1.4.1", features = ["v4"] }
[features] [features]
plugin = [] plugin = []

View File

@ -8,8 +8,8 @@ use crate::{
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use log::{trace, warn}; use log::{trace, warn};
use miette::{ErrReport, IntoDiagnostic, Result}; use miette::{ErrReport, IntoDiagnostic, Result};
use nu_cmd_base::hook::eval_hook;
use nu_cmd_base::util::get_guaranteed_cwd; use nu_cmd_base::util::get_guaranteed_cwd;
use nu_cmd_base::{hook::eval_hook, util::get_editor};
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_engine::convert_env_values; use nu_engine::convert_env_values;
use nu_parser::{lex, parse, trim_quotes_str}; use nu_parser::{lex, parse, trim_quotes_str};
@ -26,6 +26,7 @@ use reedline::{
SqliteBackedHistory, Vi, SqliteBackedHistory, Vi,
}; };
use std::{ use std::{
env::temp_dir,
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
sync::atomic::Ordering, sync::atomic::Ordering,
@ -94,6 +95,7 @@ pub fn evaluate_repl(
let mut start_time = std::time::Instant::now(); let mut start_time = std::time::Instant::now();
let mut line_editor = Reedline::create(); let mut line_editor = Reedline::create();
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
// Now that reedline is created, get the history session id and store it in engine_state // Now that reedline is created, get the history session id and store it in engine_state
store_history_id_in_engine(engine_state, &line_editor); store_history_id_in_engine(engine_state, &line_editor);
@ -330,23 +332,17 @@ pub fn evaluate_repl(
); );
start_time = std::time::Instant::now(); start_time = std::time::Instant::now();
let buffer_editor = if !config.buffer_editor.is_empty() { let buffer_editor = get_editor(engine_state, stack, Span::unknown());
Some(config.buffer_editor.clone())
} else {
stack
.get_env_var(engine_state, "EDITOR")
.map(|v| v.as_string().unwrap_or_default())
.filter(|v| !v.is_empty())
.or_else(|| {
stack
.get_env_var(engine_state, "VISUAL")
.map(|v| v.as_string().unwrap_or_default())
.filter(|v| !v.is_empty())
})
};
line_editor = if let Some(buffer_editor) = buffer_editor { line_editor = if let Ok((cmd, args)) = buffer_editor {
line_editor.with_buffer_editor(buffer_editor, "nu".into()) let mut command = std::process::Command::new(&cmd);
command.args(args).envs(
engine_state
.render_env_vars()
.into_iter()
.filter_map(|(k, v)| v.as_string().ok().map(|v| (k, v))),
);
line_editor.with_buffer_editor(command, temp_file.clone())
} else { } else {
line_editor line_editor
}; };

View File

@ -55,3 +55,72 @@ pub fn process_range(range: &Range) -> Result<(isize, isize), MakeRangeError> {
Ok((start, end)) Ok((start, end))
} }
const HELP_MSG: &str = "Nushell's config file can be found with the command: $nu.config-path. \
For more help: (https://nushell.sh/book/configuration.html#configurations-with-built-in-commands)";
fn get_editor_commandline(
value: &Value,
var_name: &str,
) -> Result<(String, Vec<String>), ShellError> {
match value {
Value::String { val, .. } if !val.is_empty() => Ok((val.to_string(), Vec::new())),
Value::List { vals, .. } if !vals.is_empty() => {
let mut editor_cmd = vals.iter().map(|l| l.as_string());
match editor_cmd.next().transpose()? {
Some(editor) if !editor.is_empty() => {
let params = editor_cmd.collect::<Result<_, ShellError>>()?;
Ok((editor, params))
}
_ => Err(ShellError::GenericError(
"Editor executable is missing".into(),
"Set the first element to an executable".into(),
Some(value.span()),
Some(HELP_MSG.into()),
vec![],
)),
}
}
Value::String { .. } | Value::List { .. } => Err(ShellError::GenericError(
format!("{var_name} should be a non-empty string or list<String>"),
"Specify an executable here".into(),
Some(value.span()),
Some(HELP_MSG.into()),
vec![],
)),
x => Err(ShellError::CantConvert {
to_type: "string or list<string>".into(),
from_type: x.get_type().to_string(),
span: value.span(),
help: None,
}),
}
}
pub fn get_editor(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
) -> Result<(String, Vec<String>), ShellError> {
let config = engine_state.get_config();
let env_vars = stack.get_env_vars(engine_state);
if let Ok(buff_editor) =
get_editor_commandline(&config.buffer_editor, "$env.config.buffer_editor")
{
Ok(buff_editor)
} else if let Some(value) = env_vars.get("EDITOR") {
get_editor_commandline(value, "$env.EDITOR")
} else if let Some(value) = env_vars.get("VISUAL") {
get_editor_commandline(value, "$env.VISUAL")
} else {
Err(ShellError::GenericError(
"No editor configured".into(),
"Please specify one via `$env.config.buffer_editor` or `$env.EDITOR`/`$env.VISUAL`"
.into(),
Some(span),
Some(HELP_MSG.into()),
vec![],
))
}
}

View File

@ -5,7 +5,8 @@ use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
}; };
use super::utils::{gen_command, get_editor}; use super::utils::gen_command;
use nu_cmd_base::util::get_editor;
#[derive(Clone)] #[derive(Clone)]
pub struct ConfigEnv; pub struct ConfigEnv;

View File

@ -5,7 +5,8 @@ use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
}; };
use super::utils::{gen_command, get_editor}; use super::utils::gen_command;
use nu_cmd_base::util::get_editor;
#[derive(Clone)] #[derive(Clone)]
pub struct ConfigNu; pub struct ConfigNu;

View File

@ -1,48 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use nu_protocol::{ use nu_protocol::{Span, Spanned};
engine::{EngineState, Stack},
ShellError, Span, Spanned,
};
use crate::ExternalCommand; use crate::ExternalCommand;
pub(crate) fn get_editor(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
) -> Result<(String, Vec<String>), ShellError> {
let config = engine_state.get_config();
let env_vars = stack.get_env_vars(engine_state);
let editor = if !config.buffer_editor.is_empty() {
Ok(config.buffer_editor.clone())
} else if let Some(value) = env_vars.get("EDITOR") {
value.as_string()
} else if let Some(value) = env_vars.get("VISUAL") {
value.as_string()
} else {
Err(ShellError::GenericError(
"No editor configured".into(),
"Please specify one via environment variables $EDITOR or $VISUAL".into(),
Some(span),
Some(
"Nushell's config file can be found with the command: $nu.config-path. For more help: (https://nushell.sh/book/configuration.html#configurations-with-built-in-commands)"
.into(),
),
vec![],
))
}?;
if let Some((a, b)) = editor.split_once(' ') {
Ok((
a.to_string(),
b.split(' ').map(|s| s.to_string()).collect::<Vec<String>>(),
))
} else {
Ok((editor, Vec::new()))
}
}
pub(crate) fn gen_command( pub(crate) fn gen_command(
span: Span, span: Span,
config_path: PathBuf, config_path: PathBuf,

View File

@ -98,7 +98,7 @@ pub struct Config {
pub hooks: Hooks, pub hooks: Hooks,
pub rm_always_trash: bool, pub rm_always_trash: bool,
pub shell_integration: bool, pub shell_integration: bool,
pub buffer_editor: String, pub buffer_editor: Value,
pub table_index_mode: TableIndexMode, pub table_index_mode: TableIndexMode,
pub cd_with_abbreviations: bool, pub cd_with_abbreviations: bool,
pub case_sensitive_completions: bool, pub case_sensitive_completions: bool,
@ -167,7 +167,7 @@ impl Default for Config {
use_grid_icons: true, use_grid_icons: true,
footer_mode: FooterMode::RowCount(25), footer_mode: FooterMode::RowCount(25),
float_precision: 2, float_precision: 2,
buffer_editor: String::new(), buffer_editor: Value::nothing(Span::unknown()),
use_ansi_coloring: true, use_ansi_coloring: true,
bracketed_paste: true, bracketed_paste: true,
edit_mode: "emacs".into(), edit_mode: "emacs".into(),
@ -1187,13 +1187,20 @@ impl Value {
"shell_integration" => { "shell_integration" => {
try_bool!(cols, vals, index, span, shell_integration); try_bool!(cols, vals, index, span, shell_integration);
} }
"buffer_editor" => { "buffer_editor" => match value {
if let Ok(v) = value.as_string() { Value::Nothing { .. } | Value::String { .. } => {
config.buffer_editor = v.to_lowercase(); config.buffer_editor = value.clone();
} else {
invalid!(Some(span), "should be a string");
} }
} Value::List { vals, .. }
if vals.iter().all(|val| matches!(val, Value::String { .. })) =>
{
config.buffer_editor = value.clone();
}
_ => {
dbg!(value);
invalid!(Some(span), "should be a string, list<string>, or null");
}
},
"show_banner" => { "show_banner" => {
try_bool!(cols, vals, index, span, show_banner); try_bool!(cols, vals, index, span, show_banner);
} }