* Use only $nu.env.PWD for getting current directory Because setting and reading to/from std::env changes the global state shich is problematic if we call `cd` from multiple threads (e.g., in a `par-each` block). With this change, when engine-q starts, it will either inherit existing PWD env var, or create a new one from `std::env::current_dir()`. Otherwise, everything that needs the current directory will get it from `$nu.env.PWD`. Each spawned external command will get its current directory per-process which should be thread-safe. One thing left to do is to patch nu-path for this as well since it uses `std::env::current_dir()` in its expansions. * Rename nu-path functions *_with is not *_relative which should be more descriptive and frees "with" for use in a followup commit. * Clone stack every each iter; Fix some commands Cloning the stack each iteration of `each` makes sure we're not reusing PWD between iterations. Some fixes in commands to make them use the new PWD. * Post-rebase cleanup, fmt, clippy * Change back _relative to _with in nu-path funcs Didn't use the idea I had for the new "_with". * Remove leftover current_dir from rebase * Add cwd sync at merge_delta() This makes sure the parser and completer always have up-to-date cwd. * Always pass absolute path to glob in ls * Do not allow PWD a relative path; Allow recovery Makes it possible to recover PWD by proceeding with the REPL cycle. * Clone stack in each also for byte/string stream * (WIP) Start moving env variables to engine state * (WIP) Move env vars to engine state (ugly) Quick and dirty code. * (WIP) Remove unused mut and args; Fmt * (WIP) Fix dataframe tests * (WIP) Fix missing args after rebase * (WIP) Clone only env vars, not the whole stack * (WIP) Add env var clone to `for` loop as well * Minor edits * Refactor merge_delta() to include stack merging. Less error-prone than doing it manually. * Clone env for each `update` command iteration * Mark env var hidden only when found in eng. state * Fix clippt warnings * Add TODO about env var reading * Do not clone empty environment in loops * Remove extra cwd collection * Split current_dir() into str and path; Fix autocd * Make completions respect PWD env var
299 lines
13 KiB
Rust
299 lines
13 KiB
Rust
use nu_engine::eval_block;
|
|
use nu_parser::{flatten_expression, parse};
|
|
use nu_protocol::{
|
|
ast::Statement,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
PipelineData, Span,
|
|
};
|
|
use reedline::Completer;
|
|
|
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
|
|
|
#[derive(Clone)]
|
|
pub struct NuCompleter {
|
|
engine_state: EngineState,
|
|
}
|
|
|
|
impl NuCompleter {
|
|
pub fn new(engine_state: EngineState) -> Self {
|
|
Self { engine_state }
|
|
}
|
|
|
|
fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
|
let offset = working_set.next_span_start();
|
|
let pos = offset + pos;
|
|
let (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
|
|
|
|
for stmt in output.stmts.into_iter() {
|
|
if let Statement::Pipeline(pipeline) = stmt {
|
|
for expr in pipeline.expressions {
|
|
if pos >= expr.span.start
|
|
&& (pos <= (line.len() + offset) || pos <= expr.span.end)
|
|
{
|
|
let possible_cmd = working_set.get_span_contents(Span {
|
|
start: expr.span.start,
|
|
end: pos,
|
|
});
|
|
|
|
let results = working_set.find_commands_by_prefix(possible_cmd);
|
|
|
|
if !results.is_empty() {
|
|
return results
|
|
.into_iter()
|
|
.map(move |x| {
|
|
(
|
|
reedline::Span {
|
|
start: expr.span.start - offset,
|
|
end: pos - offset,
|
|
},
|
|
String::from_utf8_lossy(&x).to_string(),
|
|
)
|
|
})
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
let flattened = flatten_expression(&working_set, &expr);
|
|
for flat in flattened {
|
|
if pos >= flat.0.start && pos <= flat.0.end {
|
|
match &flat.1 {
|
|
nu_parser::FlatShape::Custom(custom_completion) => {
|
|
let prefix = working_set.get_span_contents(flat.0).to_vec();
|
|
|
|
let (block, ..) = parse(
|
|
&mut working_set,
|
|
None,
|
|
custom_completion.as_bytes(),
|
|
false,
|
|
);
|
|
|
|
let mut stack = Stack::default();
|
|
let result = eval_block(
|
|
&self.engine_state,
|
|
&mut stack,
|
|
&block,
|
|
PipelineData::new(flat.0),
|
|
);
|
|
|
|
let v: Vec<_> = match result {
|
|
Ok(pd) => pd
|
|
.into_iter()
|
|
.map(move |x| {
|
|
let s = x.as_string().expect(
|
|
"FIXME: better error handling for custom completions",
|
|
);
|
|
|
|
(
|
|
reedline::Span {
|
|
start: flat.0.start - offset,
|
|
end: flat.0.end - offset,
|
|
},
|
|
s,
|
|
)
|
|
})
|
|
.filter(|x| x.1.as_bytes().starts_with(&prefix))
|
|
.collect(),
|
|
_ => vec![],
|
|
};
|
|
|
|
return v;
|
|
}
|
|
nu_parser::FlatShape::External
|
|
| nu_parser::FlatShape::InternalCall
|
|
| nu_parser::FlatShape::String => {
|
|
let prefix = working_set.get_span_contents(flat.0);
|
|
let results = working_set.find_commands_by_prefix(prefix);
|
|
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD")
|
|
{
|
|
match d.as_string() {
|
|
Ok(s) => s,
|
|
Err(_) => "".to_string(),
|
|
}
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
let prefix = String::from_utf8_lossy(prefix).to_string();
|
|
let results2 = file_path_completion(flat.0, &prefix, &cwd)
|
|
.into_iter()
|
|
.map(move |x| {
|
|
(
|
|
reedline::Span {
|
|
start: x.0.start - offset,
|
|
end: x.0.end - offset,
|
|
},
|
|
x.1,
|
|
)
|
|
});
|
|
|
|
return results
|
|
.into_iter()
|
|
.map(move |x| {
|
|
(
|
|
reedline::Span {
|
|
start: flat.0.start - offset,
|
|
end: flat.0.end - offset,
|
|
},
|
|
String::from_utf8_lossy(&x).to_string(),
|
|
)
|
|
})
|
|
.chain(results2.into_iter())
|
|
.collect();
|
|
}
|
|
nu_parser::FlatShape::Filepath
|
|
| nu_parser::FlatShape::GlobPattern
|
|
| nu_parser::FlatShape::ExternalArg => {
|
|
let prefix = working_set.get_span_contents(flat.0);
|
|
let prefix = String::from_utf8_lossy(prefix).to_string();
|
|
let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD")
|
|
{
|
|
match d.as_string() {
|
|
Ok(s) => s,
|
|
Err(_) => "".to_string(),
|
|
}
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
let results = file_path_completion(flat.0, &prefix, &cwd);
|
|
|
|
return results
|
|
.into_iter()
|
|
.map(move |x| {
|
|
(
|
|
reedline::Span {
|
|
start: x.0.start - offset,
|
|
end: x.0.end - offset,
|
|
},
|
|
x.1,
|
|
)
|
|
})
|
|
.collect();
|
|
}
|
|
_ => {
|
|
let prefix = working_set.get_span_contents(flat.0);
|
|
|
|
if prefix.starts_with(b"$") {
|
|
let mut output = vec![];
|
|
|
|
for scope in &working_set.delta.scope {
|
|
for v in &scope.vars {
|
|
if v.0.starts_with(prefix) {
|
|
output.push((
|
|
reedline::Span {
|
|
start: flat.0.start - offset,
|
|
end: flat.0.end - offset,
|
|
},
|
|
String::from_utf8_lossy(v.0).to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
for scope in &self.engine_state.scope {
|
|
for v in &scope.vars {
|
|
if v.0.starts_with(prefix) {
|
|
output.push((
|
|
reedline::Span {
|
|
start: flat.0.start - offset,
|
|
end: flat.0.end - offset,
|
|
},
|
|
String::from_utf8_lossy(v.0).to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
impl Completer for NuCompleter {
|
|
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
|
let mut output = self.completion_helper(line, pos);
|
|
|
|
output.sort_by(|a, b| a.1.cmp(&b.1));
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
fn file_path_completion(
|
|
span: nu_protocol::Span,
|
|
partial: &str,
|
|
cwd: &str,
|
|
) -> Vec<(nu_protocol::Span, String)> {
|
|
use std::path::{is_separator, Path};
|
|
|
|
let partial = if let Some(s) = partial.strip_prefix('"') {
|
|
s
|
|
} else {
|
|
partial
|
|
};
|
|
|
|
let partial = if let Some(s) = partial.strip_suffix('"') {
|
|
s
|
|
} else {
|
|
partial
|
|
};
|
|
|
|
let (base_dir_name, partial) = {
|
|
// If partial is only a word we want to search in the current dir
|
|
let (base, rest) = partial.rsplit_once(is_separator).unwrap_or((".", partial));
|
|
// On windows, this standardizes paths to use \
|
|
let mut base = base.replace(is_separator, &SEP.to_string());
|
|
|
|
// rsplit_once removes the separator
|
|
base.push(SEP);
|
|
(base, rest)
|
|
};
|
|
|
|
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
|
|
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
|
|
// which we don't want in this case (if we did, base_dir would already be ".")
|
|
if base_dir == Path::new("") {
|
|
return Vec::new();
|
|
}
|
|
|
|
if let Ok(result) = base_dir.read_dir() {
|
|
result
|
|
.filter_map(|entry| {
|
|
entry.ok().and_then(|entry| {
|
|
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
|
if matches(partial, &file_name) {
|
|
let mut path = format!("{}{}", base_dir_name, file_name);
|
|
if entry.path().is_dir() {
|
|
path.push(SEP);
|
|
file_name.push(SEP);
|
|
}
|
|
|
|
if path.contains(' ') {
|
|
path = format!("\"{}\"", path);
|
|
}
|
|
|
|
Some((span, path))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect()
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
fn matches(partial: &str, from: &str) -> bool {
|
|
from.to_ascii_lowercase()
|
|
.starts_with(&partial.to_ascii_lowercase())
|
|
}
|