mirror of
https://github.com/nushell/nushell.git
synced 2025-05-29 14:21:45 +02:00
# Description This PR introduces a `ByteStream` type which is a `Read`-able stream of bytes. Internally, it has an enum over three different byte stream sources: ```rust pub enum ByteStreamSource { Read(Box<dyn Read + Send + 'static>), File(File), Child(ChildProcess), } ``` This is in comparison to the current `RawStream` type, which is an `Iterator<Item = Vec<u8>>` and has to allocate for each read chunk. Currently, `PipelineData::ExternalStream` serves a weird dual role where it is either external command output or a wrapper around `RawStream`. `ByteStream` makes this distinction more clear (via `ByteStreamSource`) and replaces `PipelineData::ExternalStream` in this PR: ```rust pub enum PipelineData { Empty, Value(Value, Option<PipelineMetadata>), ListStream(ListStream, Option<PipelineMetadata>), ByteStream(ByteStream, Option<PipelineMetadata>), } ``` The PR is relatively large, but a decent amount of it is just repetitive changes. This PR fixes #7017, fixes #10763, and fixes #12369. This PR also improves performance when piping external commands. Nushell should, in most cases, have competitive pipeline throughput compared to, e.g., bash. | Command | Before (MB/s) | After (MB/s) | Bash (MB/s) | | -------------------------------------------------- | -------------:| ------------:| -----------:| | `throughput \| rg 'x'` | 3059 | 3744 | 3739 | | `throughput \| nu --testbin relay o> /dev/null` | 3508 | 8087 | 8136 | # User-Facing Changes - This is a breaking change for the plugin communication protocol, because the `ExternalStreamInfo` was replaced with `ByteStreamInfo`. Plugins now only have to deal with a single input stream, as opposed to the previous three streams: stdout, stderr, and exit code. - The output of `describe` has been changed for external/byte streams. - Temporary breaking change: `bytes starts-with` no longer works with byte streams. This is to keep the PR smaller, and `bytes ends-with` already does not work on byte streams. - If a process core dumped, then instead of having a `Value::Error` in the `exit_code` column of the output returned from `complete`, it now is a `Value::Int` with the negation of the signal number. # After Submitting - Update docs and book as necessary - Release notes (e.g., plugin protocol changes) - Adapt/convert commands to work with byte streams (high priority is `str length`, `bytes starts-with`, and maybe `bytes ends-with`). - Refactor the `tee` code, Devyn has already done some work on this. --------- Co-authored-by: Devyn Cairns <devyn.cairns@gmail.com>
176 lines
5.5 KiB
Rust
176 lines
5.5 KiB
Rust
use nu_engine::eval_block;
|
|
use nu_protocol::{
|
|
debugger::WithoutDebug,
|
|
engine::{EngineState, Stack},
|
|
IntoPipelineData, Span, Value,
|
|
};
|
|
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
|
|
use std::sync::Arc;
|
|
|
|
const SELECTION_CHAR: char = '!';
|
|
|
|
pub struct NuMenuCompleter {
|
|
block_id: usize,
|
|
span: Span,
|
|
stack: Stack,
|
|
engine_state: Arc<EngineState>,
|
|
only_buffer_difference: bool,
|
|
}
|
|
|
|
impl NuMenuCompleter {
|
|
pub fn new(
|
|
block_id: usize,
|
|
span: Span,
|
|
stack: Stack,
|
|
engine_state: Arc<EngineState>,
|
|
only_buffer_difference: bool,
|
|
) -> Self {
|
|
Self {
|
|
block_id,
|
|
span,
|
|
stack: stack.reset_out_dest().capture(),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Completer for NuMenuCompleter {
|
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
|
let parsed = parse_selection_char(line, SELECTION_CHAR);
|
|
|
|
let block = self.engine_state.get_block(self.block_id);
|
|
|
|
if let Some(buffer) = block.signature.get_positional(0) {
|
|
if let Some(buffer_id) = &buffer.var_id {
|
|
let line_buffer = Value::string(parsed.remainder, self.span);
|
|
self.stack.add_var(*buffer_id, line_buffer);
|
|
}
|
|
}
|
|
|
|
if let Some(position) = block.signature.get_positional(1) {
|
|
if let Some(position_id) = &position.var_id {
|
|
let line_buffer = Value::int(pos as i64, self.span);
|
|
self.stack.add_var(*position_id, line_buffer);
|
|
}
|
|
}
|
|
|
|
let input = Value::nothing(self.span).into_pipeline_data();
|
|
|
|
let res = eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, block, input);
|
|
|
|
if let Ok(values) = res.and_then(|data| data.into_value(self.span)) {
|
|
convert_to_suggestions(values, line, pos, self.only_buffer_difference)
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn convert_to_suggestions(
|
|
value: Value,
|
|
line: &str,
|
|
pos: usize,
|
|
only_buffer_difference: bool,
|
|
) -> Vec<Suggestion> {
|
|
match value {
|
|
Value::Record { val, .. } => {
|
|
let text = val
|
|
.get("value")
|
|
.and_then(|val| val.coerce_string().ok())
|
|
.unwrap_or_else(|| "No value key".to_string());
|
|
|
|
let description = val
|
|
.get("description")
|
|
.and_then(|val| val.coerce_string().ok());
|
|
|
|
let span = match val.get("span") {
|
|
Some(Value::Record { val: span, .. }) => {
|
|
let start = span.get("start").and_then(|val| val.as_int().ok());
|
|
let end = span.get("end").and_then(|val| val.as_int().ok());
|
|
match (start, end) {
|
|
(Some(start), Some(end)) => {
|
|
let start = start.min(end);
|
|
reedline::Span {
|
|
start: start as usize,
|
|
end: end as usize,
|
|
}
|
|
}
|
|
_ => reedline::Span {
|
|
start: if only_buffer_difference {
|
|
pos - line.len()
|
|
} else {
|
|
0
|
|
},
|
|
end: if only_buffer_difference {
|
|
pos
|
|
} else {
|
|
line.len()
|
|
},
|
|
},
|
|
}
|
|
}
|
|
_ => reedline::Span {
|
|
start: if only_buffer_difference {
|
|
pos - line.len()
|
|
} else {
|
|
0
|
|
},
|
|
end: if only_buffer_difference {
|
|
pos
|
|
} else {
|
|
line.len()
|
|
},
|
|
},
|
|
};
|
|
|
|
let extra = match val.get("extra") {
|
|
Some(Value::List { vals, .. }) => {
|
|
let extra: Vec<String> = vals
|
|
.iter()
|
|
.filter_map(|extra| match extra {
|
|
Value::String { val, .. } => Some(val.clone()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
Some(extra)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
vec![Suggestion {
|
|
value: text,
|
|
description,
|
|
style: None,
|
|
extra,
|
|
span,
|
|
append_whitespace: false,
|
|
}]
|
|
}
|
|
Value::List { vals, .. } => vals
|
|
.into_iter()
|
|
.flat_map(|val| convert_to_suggestions(val, line, pos, only_buffer_difference))
|
|
.collect(),
|
|
_ => vec![Suggestion {
|
|
value: format!("Not a record: {value:?}"),
|
|
description: None,
|
|
style: None,
|
|
extra: None,
|
|
span: reedline::Span {
|
|
start: if only_buffer_difference {
|
|
pos - line.len()
|
|
} else {
|
|
0
|
|
},
|
|
end: if only_buffer_difference {
|
|
pos
|
|
} else {
|
|
line.len()
|
|
},
|
|
},
|
|
append_whitespace: false,
|
|
}],
|
|
}
|
|
}
|