Add which command, add external completions, and builtin var completions (#782)

* Add which and external completions

* WIP

* Finish up external and var completions

* fix windows
This commit is contained in:
JT 2022-01-20 13:02:53 -05:00 committed by GitHub
parent d4b6b4b09a
commit 33ffb2c39a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 490 additions and 101 deletions

12
Cargo.lock generated
View File

@ -1430,6 +1430,15 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89"
[[package]]
name = "is_executable"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8"
dependencies = [
"winapi",
]
[[package]]
name = "itertools"
version = "0.10.3"
@ -1890,6 +1899,7 @@ dependencies = [
name = "nu-cli"
version = "0.1.0"
dependencies = [
"is_executable",
"log",
"miette",
"nu-ansi-term",
@ -1937,6 +1947,7 @@ dependencies = [
"indexmap",
"itertools",
"lazy_static",
"log",
"lscolors",
"md-5",
"meval",
@ -1980,6 +1991,7 @@ dependencies = [
"url",
"users",
"uuid",
"which",
"zip",
]

View File

@ -63,6 +63,7 @@ default = [
"plugin",
"inc",
"example",
"which"
]
stable = ["default"]
@ -80,6 +81,8 @@ wasi = ["inc"]
inc = ["nu_plugin_inc"]
example = ["nu_plugin_example"]
which = ["nu-command/which"]
# Extra
gstat = ["nu_plugin_gstat"]

View File

@ -16,3 +16,4 @@ miette = { version = "3.0.0", features = ["fancy"] }
thiserror = "1.0.29"
reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
log = "0.4"
is_executable = "1.0.1"

View File

@ -19,6 +19,170 @@ impl NuCompleter {
Self { engine_state }
}
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
let mut executables = vec![];
let paths;
#[cfg(windows)]
{
paths = self.engine_state.env_vars.get("Path");
}
#[cfg(not(windows))]
{
paths = self.engine_state.env_vars.get("PATH");
}
if let Some(paths) = paths {
if let Ok(paths) = paths.as_list() {
for path in paths {
let path = path.as_string().unwrap_or_default();
if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() {
if !executables.contains(
&item
.path()
.file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_default(),
) && matches!(
item.path()
.file_name()
.map(|x| x.to_string_lossy().starts_with(prefix)),
Some(true)
) && is_executable::is_executable(&item.path())
{
if let Ok(name) = item.file_name().into_string() {
executables.push(name);
}
}
}
}
}
}
}
executables
}
fn complete_variables(
&self,
working_set: &StateWorkingSet,
prefix: &[u8],
span: Span,
offset: usize,
) -> Vec<(reedline::Span, String)> {
let mut output = vec![];
let builtins = ["$nu", "$scope", "$in", "$config", "$env"];
for builtin in builtins {
if builtin.as_bytes().starts_with(prefix) {
output.push((
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
builtin.to_string(),
));
}
}
for scope in &working_set.delta.scope {
for v in &scope.vars {
if v.0.starts_with(prefix) {
output.push((
reedline::Span {
start: span.start - offset,
end: span.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: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(v.0).to_string(),
));
}
}
}
output.dedup();
output
}
fn complete_filepath_and_commands(
&self,
working_set: &StateWorkingSet,
span: Span,
offset: usize,
) -> Vec<(reedline::Span, String)> {
let prefix = working_set.get_span_contents(span);
let results = working_set
.find_commands_by_prefix(prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
String::from_utf8_lossy(&x).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 prefix = String::from_utf8_lossy(prefix).to_string();
let results_paths = file_path_completion(span, &prefix, &cwd)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
x.1,
)
});
let results_external =
self.external_command_completion(&prefix)
.into_iter()
.map(move |x| {
(
reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
x,
)
});
results
.chain(results_paths.into_iter())
.chain(results_external.into_iter())
.collect()
}
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();
@ -28,35 +192,20 @@ impl NuCompleter {
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 {
let prefix = working_set.get_span_contents(flat.0);
if prefix.starts_with(b"$") {
return self.complete_variables(
&working_set,
prefix,
flat.0,
offset,
);
}
match &flat.1 {
nu_parser::FlatShape::Custom(custom_completion) => {
let prefix = working_set.get_span_contents(flat.0).to_vec();
@ -81,8 +230,8 @@ impl NuCompleter {
.into_iter()
.map(move |x| {
let s = x.as_string().expect(
"FIXME: better error handling for custom completions",
);
"FIXME: better error handling for custom completions",
);
(
reedline::Span {
@ -102,44 +251,11 @@ impl NuCompleter {
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();
return self.complete_filepath_and_commands(
&working_set,
flat.0,
offset,
);
}
nu_parser::FlatShape::Filepath
| nu_parser::FlatShape::GlobPattern
@ -171,41 +287,7 @@ impl NuCompleter {
})
.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;
}
}
_ => {}
}
}
}

View File

@ -72,6 +72,8 @@ encoding_rs = "0.8.30"
num = { version = "0.4.0", optional = true }
reqwest = {version = "0.11", features = ["blocking"] }
mime = "0.3.16"
log = "0.4.14"
which = { version = "4.2.2", optional = true }
[target.'cfg(unix)'.dependencies]
umask = "1.0.0"

View File

@ -112,6 +112,9 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Sys,
};
#[cfg(feature = "which")]
bind_command! { Which };
// Strings
bind_command! {
BuildString,

View File

@ -2,8 +2,10 @@ mod benchmark;
mod ps;
mod run_external;
mod sys;
mod which_;
pub use benchmark::Benchmark;
pub use ps::Ps;
pub use run_external::{External, ExternalCommand};
pub use sys::Sys;
pub use which_::Which;

View File

@ -0,0 +1,256 @@
use itertools::Itertools;
use log::trace;
use nu_engine::CallExt;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
Spanned, SyntaxShape, Value,
};
#[derive(Clone)]
pub struct Which;
impl Command for Which {
fn name(&self) -> &str {
"which"
}
fn signature(&self) -> Signature {
Signature::build("which")
.required("application", SyntaxShape::String, "application")
.rest("rest", SyntaxShape::String, "additional applications")
.switch("all", "list all executables", Some('a'))
.category(Category::System)
}
fn usage(&self) -> &str {
"Finds a program file, alias or custom command."
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
which(engine_state, stack, call)
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Find if the 'myapp' application is available",
example: "which myapp",
result: None,
}]
}
}
/// Shortcuts for creating an entry to the output table
fn entry(arg: impl Into<String>, path: Value, builtin: bool, span: Span) -> Value {
let mut cols = vec![];
let mut vals = vec![];
cols.push("arg".to_string());
vals.push(Value::string(arg.into(), span));
cols.push("path".to_string());
vals.push(path);
cols.push("builtin".to_string());
vals.push(Value::Bool { val: builtin, span });
Value::Record { cols, vals, span }
}
macro_rules! create_entry {
($arg:expr, $path:expr, $span:expr, $is_builtin:expr) => {
entry(
$arg.clone(),
Value::string($path.to_string(), $span),
$is_builtin,
$span,
)
};
}
fn get_entries_in_aliases(engine_state: &EngineState, name: &str, span: Span) -> Vec<Value> {
let aliases = engine_state.find_aliases(name);
let aliases = aliases
.into_iter()
.map(|spans| {
spans
.into_iter()
.map(|span| {
String::from_utf8_lossy(engine_state.get_span_contents(&span)).to_string()
})
.join(" ")
})
.map(|alias| create_entry!(name, format!("Nushell alias: {}", alias), span, false))
.collect::<Vec<_>>();
trace!("Found {} aliases", aliases.len());
aliases
}
fn get_entries_in_custom_command(engine_state: &EngineState, name: &str, span: Span) -> Vec<Value> {
let custom_commands = engine_state.find_custom_commands(name);
custom_commands
.into_iter()
.map(|_| create_entry!(name, "Nushell custom command", span, false))
.collect::<Vec<_>>()
}
fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option<Value> {
if engine_state.find_decl(name.as_bytes()).is_some() {
Some(create_entry!(name, "Nushell built-in command", span, true))
} else {
None
}
}
fn get_entries_in_nu(
engine_state: &EngineState,
name: &str,
span: Span,
skip_after_first_found: bool,
) -> Vec<Value> {
let mut all_entries = vec![];
all_entries.extend(get_entries_in_aliases(engine_state, name, span));
if !all_entries.is_empty() && skip_after_first_found {
return all_entries;
}
all_entries.extend(get_entries_in_custom_command(engine_state, name, span));
if !all_entries.is_empty() && skip_after_first_found {
return all_entries;
}
if let Some(entry) = get_entry_in_commands(engine_state, name, span) {
all_entries.push(entry);
}
all_entries
}
#[allow(unused)]
macro_rules! entry_path {
($arg:expr, $path:expr, $span:expr) => {
entry($arg.clone(), Value::string($path, $span), false, $span)
};
}
#[cfg(feature = "which")]
fn get_first_entry_in_path(item: &str, span: Span) -> Option<Value> {
which::which(item)
.map(|path| entry_path!(item, path.to_string_lossy().to_string(), span))
.ok()
}
#[cfg(not(feature = "which"))]
fn get_first_entry_in_path(_: &str, _: Span) -> Option<Value> {
None
}
#[cfg(feature = "which")]
fn get_all_entries_in_path(item: &str, span: Span) -> Vec<Value> {
which::which_all(&item)
.map(|iter| {
iter.map(|path| entry_path!(item, path.to_string_lossy().to_string(), span))
.collect()
})
.unwrap_or_default()
}
#[cfg(not(feature = "which"))]
fn get_all_entries_in_path(_: &str, _: Span) -> Vec<Value> {
vec![]
}
#[derive(Debug)]
struct WhichArgs {
applications: Vec<Spanned<String>>,
all: bool,
}
fn which_single(application: Spanned<String>, all: bool, engine_state: &EngineState) -> Vec<Value> {
let (external, prog_name) = if application.item.starts_with('^') {
(true, application.item[1..].to_string())
} else {
(false, application.item.clone())
};
//If prog_name is an external command, don't search for nu-specific programs
//If all is false, we can save some time by only searching for the first matching
//program
//This match handles all different cases
match (all, external) {
(true, true) => get_all_entries_in_path(&prog_name, application.span),
(true, false) => {
let mut output: Vec<Value> = vec![];
output.extend(get_entries_in_nu(
engine_state,
&prog_name,
application.span,
false,
));
output.extend(get_all_entries_in_path(&prog_name, application.span));
output
}
(false, true) => {
if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) {
return vec![entry];
}
vec![]
}
(false, false) => {
let nu_entries = get_entries_in_nu(engine_state, &prog_name, application.span, true);
if !nu_entries.is_empty() {
return vec![nu_entries[0].clone()];
} else if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) {
return vec![entry];
}
vec![]
}
}
}
fn which(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
) -> Result<PipelineData, ShellError> {
let which_args = WhichArgs {
applications: call.rest(engine_state, stack, 0)?,
all: call.has_flag("all"),
};
let ctrlc = engine_state.ctrlc.clone();
if which_args.applications.is_empty() {
return Err(ShellError::MissingParameter(
"application".into(),
call.head,
));
}
let mut output = vec![];
for app in which_args.applications {
let values = which_single(app, which_args.all, engine_state);
output.extend(values);
}
Ok(output.into_iter().into_pipeline_data(ctrlc))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() {
crate::test_examples(Which)
}
}

View File

@ -344,6 +344,34 @@ impl EngineState {
}
}
pub fn find_aliases(&self, name: &str) -> Vec<Vec<Span>> {
let mut output = vec![];
for frame in &self.scope {
if let Some(alias) = frame.aliases.get(name.as_bytes()) {
output.push(alias.clone());
}
}
output
}
pub fn find_custom_commands(&self, name: &str) -> Vec<Block> {
let mut output = vec![];
for frame in &self.scope {
if let Some(decl_id) = frame.decls.get(name.as_bytes()) {
let decl = self.get_decl(*decl_id);
if let Some(block_id) = decl.get_block_id() {
output.push(self.get_block(block_id).clone());
}
}
}
output
}
pub fn find_decl(&self, name: &[u8]) -> Option<DeclId> {
let mut visibility: Visibility = Visibility::new();