forked from extern/nushell
This is an attempt to implement a new `Value::LazyRecord` variant for performance reasons. `LazyRecord` is like a regular `Record`, but it's possible to access individual columns without evaluating other columns. I've implemented `LazyRecord` for the special `$nu` variable; accessing `$nu` is relatively slow because of all the information in `scope`, and [`$nu` accounts for about 2/3 of Nu's startup time on Linux](https://github.com/nushell/nushell/issues/6677#issuecomment-1364618122). ### Benchmarks I ran some benchmarks on my desktop (Linux, 12900K) and the results are very pleasing. Nu's time to start up and run a command (`cargo build --release; hyperfine 'target/release/nu -c "echo \"Hello, world!\""' --shell=none --warmup 10`) goes from **8.8ms to 3.2ms, about 2.8x faster**. Tests are also much faster! Running `cargo nextest` (with our very slow `proptest` tests disabled) goes from **7.2s to 4.4s (1.6x faster)**, because most tests involve launching a new instance of Nu. ### Design (updated) I've added a new `LazyRecord` trait and added a `Value` variant wrapping those trait objects, much like `CustomValue`. `LazyRecord` implementations must implement these 2 functions: ```rust // All column names fn column_names(&self) -> Vec<&'static str>; // Get 1 specific column value fn get_column_value(&self, column: &str) -> Result<Value, ShellError>; ``` ### Serializability `Value` variants must implement `Serializable` and `Deserializable`, which poses some problems because I want to use unserializable things like `EngineState` in `LazyRecord`s. To work around this, I basically lie to the type system: 1. Add `#[typetag::serde(tag = "type")]` to `LazyRecord` to make it serializable 2. Any unserializable fields in `LazyRecord` implementations get marked with `#[serde(skip)]` 3. At the point where a `LazyRecord` normally would get serialized and sent to a plugin, I instead collect it into a regular `Value::Record` (which can be serialized)
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
use crate::completions::{Completer, CompletionOptions};
|
|
use nu_engine::eval_variable;
|
|
use nu_protocol::{
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
Span, Value,
|
|
};
|
|
|
|
use reedline::Suggestion;
|
|
use std::str;
|
|
use std::sync::Arc;
|
|
|
|
use super::MatchAlgorithm;
|
|
|
|
#[derive(Clone)]
|
|
pub struct VariableCompletion {
|
|
engine_state: Arc<EngineState>, // TODO: Is engine state necessary? It's already a part of working set in fetch()
|
|
stack: Stack,
|
|
var_context: (Vec<u8>, Vec<Vec<u8>>), // tuple with $var and the sublevels (.b.c.d)
|
|
}
|
|
|
|
impl VariableCompletion {
|
|
pub fn new(
|
|
engine_state: Arc<EngineState>,
|
|
stack: Stack,
|
|
var_context: (Vec<u8>, Vec<Vec<u8>>),
|
|
) -> Self {
|
|
Self {
|
|
engine_state,
|
|
stack,
|
|
var_context,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Completer for VariableCompletion {
|
|
fn fetch(
|
|
&mut self,
|
|
working_set: &StateWorkingSet,
|
|
prefix: Vec<u8>,
|
|
span: Span,
|
|
offset: usize,
|
|
_: usize,
|
|
options: &CompletionOptions,
|
|
) -> Vec<Suggestion> {
|
|
let mut output = vec![];
|
|
let builtins = ["$nu", "$in", "$env", "$nothing"];
|
|
let var_str = std::str::from_utf8(&self.var_context.0)
|
|
.unwrap_or("")
|
|
.to_lowercase();
|
|
let var_id = working_set.find_variable(&self.var_context.0);
|
|
let current_span = reedline::Span {
|
|
start: span.start - offset,
|
|
end: span.end - offset,
|
|
};
|
|
let sublevels_count = self.var_context.1.len();
|
|
|
|
// Completions for the given variable
|
|
if !var_str.is_empty() {
|
|
// Completion for $env.<tab>
|
|
if var_str.as_str() == "$env" {
|
|
let env_vars = self.stack.get_env_vars(&self.engine_state);
|
|
|
|
// Return nested values
|
|
if sublevels_count > 0 {
|
|
// Extract the target var ($env.<target-var>)
|
|
let target_var = self.var_context.1[0].clone();
|
|
let target_var_str =
|
|
str::from_utf8(&target_var).unwrap_or_default().to_string();
|
|
|
|
// Everything after the target var is the nested level ($env.<target-var>.<nested_levels>...)
|
|
let nested_levels: Vec<Vec<u8>> =
|
|
self.var_context.1.clone().into_iter().skip(1).collect();
|
|
|
|
if let Some(val) = env_vars.get(&target_var_str) {
|
|
for suggestion in
|
|
nested_suggestions(val.clone(), nested_levels, current_span)
|
|
{
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
suggestion.value.as_bytes(),
|
|
&prefix,
|
|
) {
|
|
output.push(suggestion);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
} else {
|
|
// No nesting provided, return all env vars
|
|
for env_var in env_vars {
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
env_var.0.as_bytes(),
|
|
&prefix,
|
|
) {
|
|
output.push(Suggestion {
|
|
value: env_var.0,
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
|
|
// Completions for $nu.<tab>
|
|
if var_str.as_str() == "$nu" {
|
|
// Eval nu var
|
|
if let Ok(nuval) = eval_variable(
|
|
&self.engine_state,
|
|
&self.stack,
|
|
nu_protocol::NU_VARIABLE_ID,
|
|
nu_protocol::Span::new(current_span.start, current_span.end),
|
|
) {
|
|
for suggestion in
|
|
nested_suggestions(nuval, self.var_context.1.clone(), current_span)
|
|
{
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
suggestion.value.as_bytes(),
|
|
&prefix,
|
|
) {
|
|
output.push(suggestion);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
|
|
// Completion other variable types
|
|
if let Some(var_id) = var_id {
|
|
// Extract the variable value from the stack
|
|
let var = self.stack.get_var(var_id, Span::new(span.start, span.end));
|
|
|
|
// If the value exists and it's of type Record
|
|
if let Ok(value) = var {
|
|
for suggestion in
|
|
nested_suggestions(value, self.var_context.1.clone(), current_span)
|
|
{
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
suggestion.value.as_bytes(),
|
|
&prefix,
|
|
) {
|
|
output.push(suggestion);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Variable completion (e.g: $en<tab> to complete $env)
|
|
for builtin in builtins {
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
builtin.as_bytes(),
|
|
&prefix,
|
|
) {
|
|
output.push(Suggestion {
|
|
value: builtin.to_string(),
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
// TODO: The following can be refactored (see find_commands_by_predicate() used in
|
|
// command_completions).
|
|
let mut removed_overlays = vec![];
|
|
// Working set scope vars
|
|
for scope_frame in working_set.delta.scope.iter().rev() {
|
|
for overlay_frame in scope_frame
|
|
.active_overlays(&mut removed_overlays)
|
|
.iter()
|
|
.rev()
|
|
{
|
|
for v in &overlay_frame.vars {
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
v.0,
|
|
&prefix,
|
|
) {
|
|
output.push(Suggestion {
|
|
value: String::from_utf8_lossy(v.0).to_string(),
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Permanent state vars
|
|
// for scope in &self.engine_state.scope {
|
|
for overlay_frame in self
|
|
.engine_state
|
|
.active_overlays(&removed_overlays)
|
|
.iter()
|
|
.rev()
|
|
{
|
|
for v in &overlay_frame.vars {
|
|
if options.match_algorithm.matches_u8_insensitive(
|
|
options.case_sensitive,
|
|
v.0,
|
|
&prefix,
|
|
) {
|
|
output.push(Suggestion {
|
|
value: String::from_utf8_lossy(v.0).to_string(),
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
output.dedup(); // TODO: Removes only consecutive duplicates, is it intended?
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
// Find recursively the values for sublevels
|
|
// if no sublevels are set it returns the current value
|
|
fn nested_suggestions(
|
|
val: Value,
|
|
sublevels: Vec<Vec<u8>>,
|
|
current_span: reedline::Span,
|
|
) -> Vec<Suggestion> {
|
|
let mut output: Vec<Suggestion> = vec![];
|
|
let value = recursive_value(val, sublevels);
|
|
|
|
match value {
|
|
Value::Record {
|
|
cols,
|
|
vals: _,
|
|
span: _,
|
|
} => {
|
|
// Add all the columns as completion
|
|
for item in cols {
|
|
output.push(Suggestion {
|
|
value: item,
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
|
|
output
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// Add all the columns as completion
|
|
for column_name in val.column_names() {
|
|
output.push(Suggestion {
|
|
value: column_name.to_string(),
|
|
description: None,
|
|
extra: None,
|
|
span: current_span,
|
|
append_whitespace: false,
|
|
});
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
_ => output,
|
|
}
|
|
}
|
|
|
|
// Extracts the recursive value (e.g: $var.a.b.c)
|
|
fn recursive_value(val: Value, sublevels: Vec<Vec<u8>>) -> Value {
|
|
// Go to next sublevel
|
|
if let Some(next_sublevel) = sublevels.clone().into_iter().next() {
|
|
match val {
|
|
Value::Record {
|
|
cols,
|
|
vals,
|
|
span: _,
|
|
} => {
|
|
for item in cols.into_iter().zip(vals.into_iter()) {
|
|
// Check if index matches with sublevel
|
|
if item.0.as_bytes().to_vec() == next_sublevel {
|
|
// If matches try to fetch recursively the next
|
|
return recursive_value(item.1, sublevels.into_iter().skip(1).collect());
|
|
}
|
|
}
|
|
|
|
// Current sublevel value not found
|
|
return Value::Nothing {
|
|
span: Span::unknown(),
|
|
};
|
|
}
|
|
_ => return val,
|
|
}
|
|
}
|
|
|
|
val
|
|
}
|
|
|
|
impl MatchAlgorithm {
|
|
pub fn matches_u8_insensitive(&self, sensitive: bool, haystack: &[u8], needle: &[u8]) -> bool {
|
|
if sensitive {
|
|
self.matches_u8(haystack, needle)
|
|
} else {
|
|
self.matches_u8(&haystack.to_ascii_lowercase(), &needle.to_ascii_lowercase())
|
|
}
|
|
}
|
|
}
|