Move CLI related commands to nu-cli (#8832)

# Description

Part of the larger cratification effort.

Moves all `reedline` or shell line editor specific commands to `nu-cli`.

## From `nu-cmd-lang`:
- `commandline`
- This shouldn't have moved there. Doesn't directly depend on reedline
but assumes parts in the engine state that are specific to the use of
reedline or a REPL

## From `nu-command`:
- `keybindings` and subcommands
  - `keybindings default`
  - `keybindings list`
  - `keybindings listen`
    - very `reedline` specific
- `history`
  - needs `reedline`
- `history session`

## internal use
Instead of having a separate `create_default_context()` that calls
`nu-command`'s `create_default_context()`, I added a `add_cli_context()`
that updates an `EngineState`


# User-Facing Changes

None

## Build time comparison

`cargo build --timings` from a `cargo clean --profile dev`

### total
main: 64 secs
this: 59 secs

### `nu-command` build time

branch | total| codegen | fraction  
---|---|---|---
main | 14.0s | 6.2s | (44%)
this | 12.5s | 5.5s | (44%)

`nu-cli` depends on `nu-command` at the moment.
Thus it is built during the code-gen phase of `nu-command` (on 16
virtual cores)

# Tests + Formatting

I removed the `test_example()` facilities for now as we had not run any
of the commands in an `Example` test and importing the right context for
those tests seemed more of a hassle than the duplicated
`test_examples()` implementations in `nu-cmd-lang` and `nu-command`
This commit is contained in:
Stefan Holderbach
2023-04-10 00:56:47 +02:00
committed by GitHub
parent 58b96fdede
commit 57510f2fd2
22 changed files with 49 additions and 33 deletions

View File

@ -85,7 +85,6 @@ serde_yaml = "0.9.4"
sha2 = "0.10.0"
# Disable default features b/c the default features build Git (very slow to compile)
percent-encoding = "2.2.0"
reedline = { version = "0.18.0", features = ["bashisms", "sqlite"] }
rusqlite = { version = "0.28.0", features = ["bundled"], optional = true }
sqlparser = { version = "0.32.0", features = ["serde"], optional = true }
sysinfo = "0.28.2"

View File

@ -102,9 +102,7 @@ pub fn create_default_context() -> EngineState {
// Misc
bind_command! {
History,
Tutor,
HistorySession,
};
// Path
@ -256,12 +254,8 @@ pub fn create_default_context() -> EngineState {
AnsiLink,
Clear,
Du,
KeybindingsDefault,
Input,
KeybindingsListen,
Keybindings,
Kill,
KeybindingsList,
Sleep,
TermSize,
};

View File

@ -1,264 +0,0 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, HistoryFileFormat, IntoInterruptiblePipelineData, PipelineData, ShellError,
Signature, Span, Type, Value,
};
use reedline::{
FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery,
SqliteBackedHistory,
};
#[derive(Clone)]
pub struct History;
impl Command for History {
fn name(&self) -> &str {
"history"
}
fn usage(&self) -> &str {
"Get the command history."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("history")
.input_output_types(vec![
(Type::Nothing, Type::Table(vec![])),
(Type::Nothing, Type::Nothing),
])
.allow_variants_without_examples(true)
.switch("clear", "Clears out the history entries", Some('c'))
.switch(
"long",
"Show long listing of entries for sqlite history",
Some('l'),
)
.category(Category::Misc)
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
// todo for sqlite history this command should be an alias to `open ~/.config/nushell/history.sqlite3 | get history`
if let Some(config_path) = nu_path::config_dir() {
let clear = call.has_flag("clear");
let long = call.has_flag("long");
let ctrlc = engine_state.ctrlc.clone();
let mut history_path = config_path;
history_path.push("nushell");
match engine_state.config.history_file_format {
HistoryFileFormat::Sqlite => {
history_path.push("history.sqlite3");
}
HistoryFileFormat::PlainText => {
history_path.push("history.txt");
}
}
if clear {
let _ = std::fs::remove_file(history_path);
// TODO: FIXME also clear the auxiliary files when using sqlite
Ok(PipelineData::empty())
} else {
let history_reader: Option<Box<dyn ReedlineHistory>> =
match engine_state.config.history_file_format {
HistoryFileFormat::Sqlite => SqliteBackedHistory::with_file(history_path)
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed
})
.ok(),
HistoryFileFormat::PlainText => FileBackedHistory::with_file(
engine_state.config.max_history_size as usize,
history_path,
)
.map(|inner| {
let boxed: Box<dyn ReedlineHistory> = Box::new(inner);
boxed
})
.ok(),
};
match engine_state.config.history_file_format {
HistoryFileFormat::PlainText => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward))
.ok()
})
.map(move |entries| {
entries
.into_iter()
.enumerate()
.map(move |(idx, entry)| Value::Record {
cols: vec!["command".to_string(), "index".to_string()],
vals: vec![
Value::String {
val: entry.command_line,
span: head,
},
Value::int(idx as i64, head),
],
span: head,
})
})
.ok_or(ShellError::FileNotFound(head))?
.into_pipeline_data(ctrlc)),
HistoryFileFormat::Sqlite => Ok(history_reader
.and_then(|h| {
h.search(SearchQuery::everything(SearchDirection::Forward))
.ok()
})
.map(move |entries| {
entries.into_iter().enumerate().map(move |(idx, entry)| {
create_history_record(idx, entry, long, head)
})
})
.ok_or(ShellError::FileNotFound(head))?
.into_pipeline_data(ctrlc)),
}
}
} else {
Err(ShellError::FileNotFound(head))
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
example: "history | length",
description: "Get current history length",
result: None,
},
Example {
example: "history | last 5",
description: "Show last 5 commands you have ran",
result: None,
},
Example {
example: "history | wrap cmd | where cmd =~ cargo",
description: "Search all the commands from history that contains 'cargo'",
result: None,
},
]
}
}
fn create_history_record(idx: usize, entry: HistoryItem, long: bool, head: Span) -> Value {
//1. Format all the values
//2. Create a record of either short or long columns and values
let item_id_value = Value::Int {
val: match entry.id {
Some(id) => {
let ids = id.to_string();
match ids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
span: head,
};
let start_timestamp_value = Value::String {
val: match entry.start_timestamp {
Some(time) => time.to_string(),
None => "".into(),
},
span: head,
};
let command_value = Value::String {
val: entry.command_line,
span: head,
};
let session_id_value = Value::Int {
val: match entry.session_id {
Some(sid) => {
let sids = sid.to_string();
match sids.parse::<i64>() {
Ok(i) => i,
_ => 0i64,
}
}
None => 0i64,
},
span: head,
};
let hostname_value = Value::String {
val: match entry.hostname {
Some(host) => host,
None => "".into(),
},
span: head,
};
let cwd_value = Value::String {
val: match entry.cwd {
Some(cwd) => cwd,
None => "".into(),
},
span: head,
};
let duration_value = Value::Duration {
val: match entry.duration {
Some(d) => d.as_nanos().try_into().unwrap_or(0),
None => 0,
},
span: head,
};
let exit_status_value = Value::int(entry.exit_status.unwrap_or(0), head);
let index_value = Value::int(idx as i64, head);
if long {
Value::Record {
cols: vec![
"item_id".into(),
"start_timestamp".into(),
"command".to_string(),
"session_id".into(),
"hostname".into(),
"cwd".into(),
"duration".into(),
"exit_status".into(),
"idx".to_string(),
],
vals: vec![
item_id_value,
start_timestamp_value,
command_value,
session_id_value,
hostname_value,
cwd_value,
duration_value,
exit_status_value,
index_value,
],
span: head,
}
} else {
Value::Record {
cols: vec![
"start_timestamp".into(),
"command".to_string(),
"cwd".into(),
"duration".into(),
"exit_status".into(),
],
vals: vec![
start_timestamp_value,
command_value,
cwd_value,
duration_value,
exit_status_value,
],
span: head,
}
}
}

View File

@ -1,42 +0,0 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
#[derive(Clone)]
pub struct HistorySession;
impl Command for HistorySession {
fn name(&self) -> &str {
"history session"
}
fn usage(&self) -> &str {
"Get the command history session."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("history session")
.category(Category::Misc)
.input_output_types(vec![(Type::Nothing, Type::Int)])
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "history session",
description: "Get current history session",
result: None,
}]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(Value::int(engine_state.history_session_id, call.head).into_pipeline_data())
}
}

View File

@ -1,7 +1,3 @@
mod history;
mod history_session;
mod tutor;
pub use history::History;
pub use history_session::HistorySession;
pub use tutor::Tutor;

View File

@ -4,7 +4,6 @@ mod dir_info;
mod du;
mod input;
mod kill;
mod reedline_commands;
mod sleep;
mod term_size;
@ -14,6 +13,5 @@ pub use dir_info::{DirBuilder, DirInfo, FileInfo};
pub use du::Du;
pub use input::Input;
pub use kill::Kill;
pub use reedline_commands::{Keybindings, KeybindingsDefault, KeybindingsList, KeybindingsListen};
pub use sleep::Sleep;
pub use term_size::TermSize;

View File

@ -1,53 +0,0 @@
use nu_engine::get_full_help;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
#[derive(Clone)]
pub struct Keybindings;
impl Command for Keybindings {
fn name(&self) -> &str {
"keybindings"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::String)])
}
fn usage(&self) -> &str {
"Keybindings related commands."
}
fn extra_usage(&self) -> &str {
"You must use one of the following subcommands. Using this command as-is will only produce this help message."
}
fn search_terms(&self) -> Vec<&str> {
vec!["shortcut", "hotkey"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(Value::String {
val: get_full_help(
&Keybindings.signature(),
&Keybindings.examples(),
engine_state,
stack,
self.is_parser_keyword(),
),
span: call.head,
}
.into_pipeline_data())
}
}

View File

@ -1,83 +0,0 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
};
use reedline::get_reedline_default_keybindings;
#[derive(Clone)]
pub struct KeybindingsDefault;
impl Command for KeybindingsDefault {
fn name(&self) -> &str {
"keybindings default"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
}
fn usage(&self) -> &str {
"List default keybindings."
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Get list with default keybindings",
example: "keybindings default",
result: None,
}]
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let records = get_reedline_default_keybindings()
.into_iter()
.map(|(mode, modifier, code, event)| {
let mode = Value::String {
val: mode,
span: call.head,
};
let modifier = Value::String {
val: modifier,
span: call.head,
};
let code = Value::String {
val: code,
span: call.head,
};
let event = Value::String {
val: event,
span: call.head,
};
Value::Record {
cols: vec![
"mode".to_string(),
"modifier".to_string(),
"code".to_string(),
"event".to_string(),
],
vals: vec![mode, modifier, code, event],
span: call.head,
}
})
.collect();
Ok(Value::List {
vals: records,
span: call.head,
}
.into_pipeline_data())
}
}

View File

@ -1,123 +0,0 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
};
use reedline::{
get_reedline_edit_commands, get_reedline_keybinding_modifiers, get_reedline_keycodes,
get_reedline_prompt_edit_modes, get_reedline_reedline_events,
};
#[derive(Clone)]
pub struct KeybindingsList;
impl Command for KeybindingsList {
fn name(&self) -> &str {
"keybindings list"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::Table(vec![]))])
.switch("modifiers", "list of modifiers", Some('m'))
.switch("keycodes", "list of keycodes", Some('k'))
.switch("modes", "list of edit modes", Some('o'))
.switch("events", "list of reedline event", Some('e'))
.switch("edits", "list of edit commands", Some('d'))
.category(Category::Platform)
}
fn usage(&self) -> &str {
"List available options that can be used to create keybindings."
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get list of key modifiers",
example: "keybindings list -m",
result: None,
},
Example {
description: "Get list of reedline events and edit commands",
example: "keybindings list -e -d",
result: None,
},
Example {
description: "Get list with all the available options",
example: "keybindings list",
result: None,
},
]
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let records = if call.named_len() == 0 {
let all_options = vec!["modifiers", "keycodes", "edits", "modes", "events"];
all_options
.iter()
.flat_map(|argument| get_records(argument, &call.head))
.collect()
} else {
call.named_iter()
.flat_map(|(argument, _, _)| get_records(argument.item.as_str(), &call.head))
.collect()
};
Ok(Value::List {
vals: records,
span: call.head,
}
.into_pipeline_data())
}
}
fn get_records(entry_type: &str, span: &Span) -> Vec<Value> {
let values = match entry_type {
"modifiers" => get_reedline_keybinding_modifiers().sorted(),
"keycodes" => get_reedline_keycodes().sorted(),
"edits" => get_reedline_edit_commands().sorted(),
"modes" => get_reedline_prompt_edit_modes().sorted(),
"events" => get_reedline_reedline_events().sorted(),
_ => Vec::new(),
};
values
.iter()
.map(|edit| edit.split('\n'))
.flat_map(|edit| edit.map(|edit| convert_to_record(edit, entry_type, span)))
.collect()
}
fn convert_to_record(edit: &str, entry_type: &str, span: &Span) -> Value {
let entry_type = Value::string(entry_type, *span);
let name = Value::string(edit, *span);
Value::Record {
cols: vec!["type".to_string(), "name".to_string()],
vals: vec![entry_type, name],
span: *span,
}
}
// Helper to sort a vec and return a vec
trait SortedImpl {
fn sorted(self) -> Self;
}
impl<E> SortedImpl for Vec<E>
where
E: std::cmp::Ord,
{
fn sorted(mut self) -> Self {
self.sort();
self
}
}

View File

@ -1,157 +0,0 @@
use crossterm::QueueableCommand;
use crossterm::{event::Event, event::KeyCode, event::KeyEvent, terminal};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value,
};
use std::io::{stdout, Write};
#[derive(Clone)]
pub struct KeybindingsListen;
impl Command for KeybindingsListen {
fn name(&self) -> &str {
"keybindings listen"
}
fn usage(&self) -> &str {
"Get input from the user."
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
println!("Type any key combination to see key details. Press ESC to abort.");
match print_events(engine_state) {
Ok(v) => Ok(v.into_pipeline_data()),
Err(e) => {
terminal::disable_raw_mode()?;
Err(ShellError::GenericError(
"Error with input".to_string(),
"".to_string(),
None,
Some(e.to_string()),
Vec::new(),
))
}
}
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Type and see key event codes",
example: "keybindings listen",
result: None,
}]
}
}
pub fn print_events(engine_state: &EngineState) -> Result<Value, ShellError> {
let config = engine_state.get_config();
stdout().flush()?;
terminal::enable_raw_mode()?;
let mut stdout = std::io::BufWriter::new(std::io::stderr());
loop {
let event = crossterm::event::read()?;
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
// stdout.queue(crossterm::style::Print(format!("event: {:?}", &event)))?;
// stdout.queue(crossterm::style::Print("\r\n"))?;
// Get a record
let v = print_events_helper(event)?;
// Print out the record
let o = match v {
Value::Record { cols, vals, .. } => cols
.iter()
.zip(vals.iter())
.map(|(x, y)| format!("{}: {}", x, y.into_string("", config)))
.collect::<Vec<String>>()
.join(", "),
_ => "".to_string(),
};
stdout.queue(crossterm::style::Print(o))?;
stdout.queue(crossterm::style::Print("\r\n"))?;
stdout.flush()?;
}
terminal::disable_raw_mode()?;
Ok(Value::nothing(Span::unknown()))
}
// this fn is totally ripped off from crossterm's examples
// it's really a diagnostic routine to see if crossterm is
// even seeing the events. if you press a key and no events
// are printed, it's a good chance your terminal is eating
// those events.
fn print_events_helper(event: Event) -> Result<Value, ShellError> {
if let Event::Key(KeyEvent { code, modifiers }) = event {
match code {
KeyCode::Char(c) => {
let record = Value::Record {
cols: vec![
"char".into(),
"code".into(),
"modifier".into(),
"flags".into(),
],
vals: vec![
Value::string(format!("{c}"), Span::unknown()),
Value::string(format!("{:#08x}", u32::from(c)), Span::unknown()),
Value::string(format!("{modifiers:?}"), Span::unknown()),
Value::string(format!("{modifiers:#08b}"), Span::unknown()),
],
span: Span::unknown(),
};
Ok(record)
}
_ => {
let record = Value::Record {
cols: vec!["code".into(), "modifier".into(), "flags".into()],
vals: vec![
Value::string(format!("{code:?}"), Span::unknown()),
Value::string(format!("{modifiers:?}"), Span::unknown()),
Value::string(format!("{modifiers:#08b}"), Span::unknown()),
],
span: Span::unknown(),
};
Ok(record)
}
}
} else {
let record = Value::Record {
cols: vec!["event".into()],
vals: vec![Value::string(format!("{event:?}"), Span::unknown())],
span: Span::unknown(),
};
Ok(record)
}
}
#[cfg(test)]
mod tests {
use crate::KeybindingsListen;
#[test]
fn examples_work_as_expected() {
use crate::test_examples;
test_examples(KeybindingsListen {})
}
}

View File

@ -1,9 +0,0 @@
mod keybindings;
mod keybindings_default;
mod keybindings_list;
mod keybindings_listen;
pub use keybindings::Keybindings;
pub use keybindings_default::KeybindingsDefault;
pub use keybindings_list::KeybindingsList;
pub use keybindings_listen::KeybindingsListen;