mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00:00
nu-cli: refactor completions (#5102)
This commit is contained in:
parent
121a4f06fb
commit
13869e7d52
12 changed files with 1017 additions and 701 deletions
|
@ -1,699 +0,0 @@
|
||||||
use nu_engine::eval_call;
|
|
||||||
use nu_parser::{flatten_expression, parse, trim_quotes, FlatShape};
|
|
||||||
use nu_protocol::{
|
|
||||||
ast::{Call, Expr, Expression},
|
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
|
||||||
levenshtein_distance, PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID,
|
|
||||||
};
|
|
||||||
use reedline::{Completer, Suggestion};
|
|
||||||
|
|
||||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
|
||||||
|
|
||||||
pub struct CompletionOptions {
|
|
||||||
case_sensitive: bool,
|
|
||||||
positional: bool,
|
|
||||||
sort: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CompletionOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
case_sensitive: true,
|
|
||||||
positional: true,
|
|
||||||
sort: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct NuCompleter {
|
|
||||||
engine_state: EngineState,
|
|
||||||
stack: Stack,
|
|
||||||
config: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NuCompleter {
|
|
||||||
pub fn new(engine_state: EngineState, stack: Stack, config: Option<Value>) -> Self {
|
|
||||||
Self {
|
|
||||||
engine_state,
|
|
||||||
stack,
|
|
||||||
config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
|
|
||||||
let mut executables = vec![];
|
|
||||||
|
|
||||||
let 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<Suggestion> {
|
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
|
|
||||||
|
|
||||||
for builtin in builtins {
|
|
||||||
if builtin.as_bytes().starts_with(prefix) {
|
|
||||||
output.push(Suggestion {
|
|
||||||
value: builtin.to_string(),
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for scope in &working_set.delta.scope {
|
|
||||||
for v in &scope.vars {
|
|
||||||
if v.0.starts_with(prefix) {
|
|
||||||
output.push(Suggestion {
|
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for scope in &self.engine_state.scope {
|
|
||||||
for v in &scope.vars {
|
|
||||||
if v.0.starts_with(prefix) {
|
|
||||||
output.push(Suggestion {
|
|
||||||
value: String::from_utf8_lossy(v.0).to_string(),
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output.dedup();
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete_commands(
|
|
||||||
&self,
|
|
||||||
working_set: &StateWorkingSet,
|
|
||||||
span: Span,
|
|
||||||
offset: usize,
|
|
||||||
find_externals: bool,
|
|
||||||
) -> Vec<Suggestion> {
|
|
||||||
let prefix = working_set.get_span_contents(span);
|
|
||||||
|
|
||||||
let results = working_set
|
|
||||||
.find_commands_by_prefix(prefix)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| Suggestion {
|
|
||||||
value: String::from_utf8_lossy(&x.0).to_string(),
|
|
||||||
description: x.1,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let results_aliases =
|
|
||||||
working_set
|
|
||||||
.find_aliases_by_prefix(prefix)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| Suggestion {
|
|
||||||
value: String::from_utf8_lossy(&x).to_string(),
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let prefix = working_set.get_span_contents(span);
|
|
||||||
let prefix = String::from_utf8_lossy(prefix).to_string();
|
|
||||||
let mut results = if find_externals {
|
|
||||||
let results_external =
|
|
||||||
self.external_command_completion(&prefix)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| Suggestion {
|
|
||||||
value: x,
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: span.start - offset,
|
|
||||||
end: span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for external in results_external {
|
|
||||||
if results.contains(&external) {
|
|
||||||
results.push(Suggestion {
|
|
||||||
value: format!("^{}", external.value),
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: external.span,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
results.push(external)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
} else {
|
|
||||||
results
|
|
||||||
};
|
|
||||||
|
|
||||||
results.sort_by(|a, b| {
|
|
||||||
let a_distance = levenshtein_distance(&prefix, &a.value);
|
|
||||||
let b_distance = levenshtein_distance(&prefix, &b.value);
|
|
||||||
a_distance.cmp(&b_distance)
|
|
||||||
});
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
|
||||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
|
||||||
let offset = working_set.next_span_start();
|
|
||||||
let current_line_str = line.trim().to_string();
|
|
||||||
let mut line = line.to_string();
|
|
||||||
line.insert(pos, 'a');
|
|
||||||
let line_pos = pos;
|
|
||||||
let pos = offset + pos;
|
|
||||||
let (output, _err) = parse(
|
|
||||||
&mut working_set,
|
|
||||||
Some("completer"),
|
|
||||||
line.as_bytes(),
|
|
||||||
false,
|
|
||||||
&[],
|
|
||||||
);
|
|
||||||
|
|
||||||
for pipeline in output.pipelines.into_iter() {
|
|
||||||
for expr in pipeline.expressions {
|
|
||||||
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
|
||||||
|
|
||||||
for (flat_idx, flat) in flattened.iter().enumerate() {
|
|
||||||
if pos >= flat.0.start && pos < flat.0.end {
|
|
||||||
let new_span = Span {
|
|
||||||
start: flat.0.start,
|
|
||||||
end: flat.0.end - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
|
|
||||||
prefix.remove(pos - flat.0.start);
|
|
||||||
|
|
||||||
if prefix.starts_with(b"$") {
|
|
||||||
let mut output =
|
|
||||||
self.complete_variables(&working_set, &prefix, new_span, offset);
|
|
||||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
if prefix.starts_with(b"-") {
|
|
||||||
// this might be a flag, let's see
|
|
||||||
if let Expr::Call(call) = &expr.expr {
|
|
||||||
let decl = working_set.get_decl(call.decl_id);
|
|
||||||
let sig = decl.signature();
|
|
||||||
|
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
for named in &sig.named {
|
|
||||||
let flag_desc = &named.desc;
|
|
||||||
if let Some(short) = named.short {
|
|
||||||
let mut named = vec![0; short.len_utf8()];
|
|
||||||
short.encode_utf8(&mut named);
|
|
||||||
named.insert(0, b'-');
|
|
||||||
if named.starts_with(&prefix) {
|
|
||||||
output.push(Suggestion {
|
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
|
||||||
description: Some(flag_desc.to_string()),
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: new_span.start - offset,
|
|
||||||
end: new_span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if named.long.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut named = named.long.as_bytes().to_vec();
|
|
||||||
named.insert(0, b'-');
|
|
||||||
named.insert(0, b'-');
|
|
||||||
if named.starts_with(&prefix) {
|
|
||||||
output.push(Suggestion {
|
|
||||||
value: String::from_utf8_lossy(&named).to_string(),
|
|
||||||
description: Some(flag_desc.to_string()),
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: new_span.start - offset,
|
|
||||||
end: new_span.end - offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match &flat.1 {
|
|
||||||
FlatShape::Custom(decl_id) => {
|
|
||||||
let mut stack = self.stack.clone();
|
|
||||||
|
|
||||||
// Set up our initial config to start from
|
|
||||||
if let Some(conf) = &self.config {
|
|
||||||
stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
|
|
||||||
} else {
|
|
||||||
stack.vars.insert(
|
|
||||||
CONFIG_VARIABLE_ID,
|
|
||||||
Value::Record {
|
|
||||||
cols: vec![],
|
|
||||||
vals: vec![],
|
|
||||||
span: Span { start: 0, end: 0 },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = eval_call(
|
|
||||||
&self.engine_state,
|
|
||||||
&mut stack,
|
|
||||||
&Call {
|
|
||||||
decl_id: *decl_id,
|
|
||||||
head: new_span,
|
|
||||||
positional: vec![
|
|
||||||
Expression {
|
|
||||||
span: Span { start: 0, end: 0 },
|
|
||||||
ty: Type::String,
|
|
||||||
expr: Expr::String(current_line_str),
|
|
||||||
custom_completion: None,
|
|
||||||
},
|
|
||||||
Expression {
|
|
||||||
span: Span { start: 0, end: 0 },
|
|
||||||
ty: Type::Int,
|
|
||||||
expr: Expr::Int(line_pos as i64),
|
|
||||||
custom_completion: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
named: vec![],
|
|
||||||
redirect_stdout: true,
|
|
||||||
redirect_stderr: true,
|
|
||||||
},
|
|
||||||
PipelineData::new(new_span),
|
|
||||||
);
|
|
||||||
|
|
||||||
fn map_completions<'a>(
|
|
||||||
list: impl Iterator<Item = &'a Value>,
|
|
||||||
new_span: Span,
|
|
||||||
offset: usize,
|
|
||||||
) -> Vec<Suggestion> {
|
|
||||||
list.filter_map(move |x| {
|
|
||||||
let s = x.as_string();
|
|
||||||
|
|
||||||
match s {
|
|
||||||
Ok(s) => Some(Suggestion {
|
|
||||||
value: s,
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: new_span.start - offset,
|
|
||||||
end: new_span.end - offset,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
let (completions, options) = match result {
|
|
||||||
Ok(pd) => {
|
|
||||||
let value = pd.into_value(new_span);
|
|
||||||
match &value {
|
|
||||||
Value::Record { .. } => {
|
|
||||||
let completions = value
|
|
||||||
.get_data_by_key("completions")
|
|
||||||
.and_then(|val| {
|
|
||||||
val.as_list().ok().map(|it| {
|
|
||||||
map_completions(
|
|
||||||
it.iter(),
|
|
||||||
new_span,
|
|
||||||
offset,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
let options = value.get_data_by_key("options");
|
|
||||||
|
|
||||||
let options =
|
|
||||||
if let Some(Value::Record { .. }) = &options {
|
|
||||||
let options = options.unwrap_or_default();
|
|
||||||
CompletionOptions {
|
|
||||||
case_sensitive: options
|
|
||||||
.get_data_by_key("case_sensitive")
|
|
||||||
.and_then(|val| val.as_bool().ok())
|
|
||||||
.unwrap_or(true),
|
|
||||||
positional: options
|
|
||||||
.get_data_by_key("positional")
|
|
||||||
.and_then(|val| val.as_bool().ok())
|
|
||||||
.unwrap_or(true),
|
|
||||||
sort: options
|
|
||||||
.get_data_by_key("sort")
|
|
||||||
.and_then(|val| val.as_bool().ok())
|
|
||||||
.unwrap_or(true),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CompletionOptions::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
(completions, options)
|
|
||||||
}
|
|
||||||
Value::List { vals, .. } => {
|
|
||||||
let completions =
|
|
||||||
map_completions(vals.iter(), new_span, offset);
|
|
||||||
(completions, CompletionOptions::default())
|
|
||||||
}
|
|
||||||
_ => (vec![], CompletionOptions::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (vec![], CompletionOptions::default()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut completions: Vec<Suggestion> = completions
|
|
||||||
.into_iter()
|
|
||||||
.filter(|it| {
|
|
||||||
// Minimise clones for new functionality
|
|
||||||
match (options.case_sensitive, options.positional) {
|
|
||||||
(true, true) => {
|
|
||||||
it.value.as_bytes().starts_with(&prefix)
|
|
||||||
}
|
|
||||||
(true, false) => it.value.contains(
|
|
||||||
std::str::from_utf8(&prefix).unwrap_or(""),
|
|
||||||
),
|
|
||||||
(false, positional) => {
|
|
||||||
let value = it.value.to_lowercase();
|
|
||||||
let prefix = std::str::from_utf8(&prefix)
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_lowercase();
|
|
||||||
if positional {
|
|
||||||
value.starts_with(&prefix)
|
|
||||||
} else {
|
|
||||||
value.contains(&prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if options.sort {
|
|
||||||
completions.sort_by(|a, b| a.value.cmp(&b.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return completions;
|
|
||||||
}
|
|
||||||
FlatShape::Filepath | FlatShape::GlobPattern => {
|
|
||||||
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 mut output: Vec<_> =
|
|
||||||
file_path_completion(new_span, &prefix, &cwd)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| Suggestion {
|
|
||||||
value: x.1,
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: x.0.start - offset,
|
|
||||||
end: x.0.end - offset,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// output.sort_by(|a, b| a.value.cmp(&b.value));
|
|
||||||
output.sort_by(|a, b| {
|
|
||||||
let a_distance = levenshtein_distance(&prefix, &a.value);
|
|
||||||
let b_distance = levenshtein_distance(&prefix, &b.value);
|
|
||||||
a_distance.cmp(&b_distance)
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
flat_shape => {
|
|
||||||
let last = flattened
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.skip_while(|x| x.0.end > pos)
|
|
||||||
.take_while(|x| {
|
|
||||||
matches!(
|
|
||||||
x.1,
|
|
||||||
FlatShape::InternalCall
|
|
||||||
| FlatShape::External
|
|
||||||
| FlatShape::ExternalArg
|
|
||||||
| FlatShape::Literal
|
|
||||||
| FlatShape::String
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.last();
|
|
||||||
|
|
||||||
// The last item here would be the earliest shape that could possible by part of this subcommand
|
|
||||||
let subcommands = if let Some(last) = last {
|
|
||||||
self.complete_commands(
|
|
||||||
&working_set,
|
|
||||||
Span {
|
|
||||||
start: last.0.start,
|
|
||||||
end: pos,
|
|
||||||
},
|
|
||||||
offset,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
if !subcommands.is_empty() {
|
|
||||||
return subcommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
let commands =
|
|
||||||
if matches!(flat_shape, nu_parser::FlatShape::External)
|
|
||||||
|| matches!(flat_shape, nu_parser::FlatShape::InternalCall)
|
|
||||||
|| ((new_span.end - new_span.start) == 0)
|
|
||||||
{
|
|
||||||
// we're in a gap or at a command
|
|
||||||
self.complete_commands(&working_set, new_span, offset, true)
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
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 preceding_byte = if new_span.start > offset {
|
|
||||||
working_set
|
|
||||||
.get_span_contents(Span {
|
|
||||||
start: new_span.start - 1,
|
|
||||||
end: new_span.start,
|
|
||||||
})
|
|
||||||
.to_vec()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
// let prefix = working_set.get_span_contents(flat.0);
|
|
||||||
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
|
||||||
let mut output = file_path_completion(new_span, &prefix, &cwd)
|
|
||||||
.into_iter()
|
|
||||||
.map(move |x| {
|
|
||||||
if flat_idx == 0 {
|
|
||||||
// We're in the command position
|
|
||||||
if x.1.starts_with('"')
|
|
||||||
&& !matches!(preceding_byte.get(0), Some(b'^'))
|
|
||||||
{
|
|
||||||
let trimmed = trim_quotes(x.1.as_bytes());
|
|
||||||
let trimmed =
|
|
||||||
String::from_utf8_lossy(trimmed).to_string();
|
|
||||||
let expanded =
|
|
||||||
nu_path::canonicalize_with(trimmed, &cwd);
|
|
||||||
|
|
||||||
if let Ok(expanded) = expanded {
|
|
||||||
if is_executable::is_executable(expanded) {
|
|
||||||
(x.0, format!("^{}", x.1))
|
|
||||||
} else {
|
|
||||||
(x.0, x.1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(x.0, x.1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(x.0, x.1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(x.0, x.1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(move |x| Suggestion {
|
|
||||||
value: x.1,
|
|
||||||
description: None,
|
|
||||||
extra: None,
|
|
||||||
span: reedline::Span {
|
|
||||||
start: x.0.start - offset,
|
|
||||||
end: x.0.end - offset,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.chain(subcommands.into_iter())
|
|
||||||
.chain(commands.into_iter())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
//output.dedup_by(|a, b| a.1 == b.1);
|
|
||||||
//output.sort_by(|a, b| a.value.cmp(&b.value));
|
|
||||||
output.sort_by(|a, b| {
|
|
||||||
let a_distance = levenshtein_distance(&prefix, &a.value);
|
|
||||||
let b_distance = levenshtein_distance(&prefix, &b.value);
|
|
||||||
a_distance.cmp(&b_distance)
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Completer for NuCompleter {
|
|
||||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
|
||||||
self.completion_helper(line, pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = partial.replace('\'', "");
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut results = 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()
|
|
||||||
};
|
|
||||||
|
|
||||||
results.sort_by(|a, b| {
|
|
||||||
let a_distance = levenshtein_distance(partial, &a.1);
|
|
||||||
let b_distance = levenshtein_distance(partial, &b.1);
|
|
||||||
a_distance.cmp(&b_distance)
|
|
||||||
});
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matches(partial: &str, from: &str) -> bool {
|
|
||||||
from.to_ascii_lowercase()
|
|
||||||
.starts_with(&partial.to_ascii_lowercase())
|
|
||||||
}
|
|
74
crates/nu-cli/src/completions/base.rs
Normal file
74
crates/nu-cli/src/completions/base.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::completions::{CompletionOptions, SortBy};
|
||||||
|
use nu_protocol::{engine::StateWorkingSet, levenshtein_distance, Span};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
// Completer trait represents the three stages of the completion
|
||||||
|
// fetch, filter and sort
|
||||||
|
pub trait Completer {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
pos: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions);
|
||||||
|
|
||||||
|
// Filter results using the completion options
|
||||||
|
fn filter(
|
||||||
|
&self,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
items: Vec<Suggestion>,
|
||||||
|
options: CompletionOptions,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.filter(|it| {
|
||||||
|
// Minimise clones for new functionality
|
||||||
|
match (options.case_sensitive, options.positional) {
|
||||||
|
(true, true) => it.value.as_bytes().starts_with(&prefix),
|
||||||
|
(true, false) => it
|
||||||
|
.value
|
||||||
|
.contains(std::str::from_utf8(&prefix).unwrap_or("")),
|
||||||
|
(false, positional) => {
|
||||||
|
let value = it.value.to_lowercase();
|
||||||
|
let prefix = std::str::from_utf8(&prefix).unwrap_or("").to_lowercase();
|
||||||
|
if positional {
|
||||||
|
value.starts_with(&prefix)
|
||||||
|
} else {
|
||||||
|
value.contains(&prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort is results using the completion options
|
||||||
|
fn sort(
|
||||||
|
&self,
|
||||||
|
items: Vec<Suggestion>,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
options: CompletionOptions,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
|
||||||
|
let mut filtered_items = items;
|
||||||
|
|
||||||
|
// Sort items
|
||||||
|
match options.sort_by {
|
||||||
|
SortBy::LevenshteinDistance => {
|
||||||
|
filtered_items.sort_by(|a, b| {
|
||||||
|
let a_distance = levenshtein_distance(&prefix_str, &a.value);
|
||||||
|
let b_distance = levenshtein_distance(&prefix_str, &b.value);
|
||||||
|
a_distance.cmp(&b_distance)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SortBy::Ascending => {
|
||||||
|
filtered_items.sort_by(|a, b| a.value.cmp(&b.value));
|
||||||
|
}
|
||||||
|
SortBy::None => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
filtered_items
|
||||||
|
}
|
||||||
|
}
|
268
crates/nu-cli/src/completions/command_completions.rs
Normal file
268
crates/nu-cli/src/completions/command_completions.rs
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
use crate::completions::{
|
||||||
|
file_completions::file_path_completion, Completer, CompletionOptions, SortBy,
|
||||||
|
};
|
||||||
|
use nu_parser::{trim_quotes, FlatShape};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
Span,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub struct CommandCompletion {
|
||||||
|
engine_state: EngineState,
|
||||||
|
flattened: Vec<(Span, FlatShape)>,
|
||||||
|
flat_idx: usize,
|
||||||
|
flat_shape: FlatShape,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandCompletion {
|
||||||
|
pub fn new(
|
||||||
|
engine_state: EngineState,
|
||||||
|
_: &StateWorkingSet,
|
||||||
|
flattened: Vec<(Span, FlatShape)>,
|
||||||
|
flat_idx: usize,
|
||||||
|
flat_shape: FlatShape,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
engine_state,
|
||||||
|
flattened,
|
||||||
|
flat_idx,
|
||||||
|
flat_shape,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn external_command_completion(&self, prefix: &str) -> Vec<String> {
|
||||||
|
let mut executables = vec![];
|
||||||
|
|
||||||
|
let 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_commands(
|
||||||
|
&self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
find_externals: bool,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
let prefix = working_set.get_span_contents(span);
|
||||||
|
|
||||||
|
let results = working_set
|
||||||
|
.find_commands_by_prefix(prefix)
|
||||||
|
.into_iter()
|
||||||
|
.map(move |x| Suggestion {
|
||||||
|
value: String::from_utf8_lossy(&x.0).to_string(),
|
||||||
|
description: x.1,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let results_aliases =
|
||||||
|
working_set
|
||||||
|
.find_aliases_by_prefix(prefix)
|
||||||
|
.into_iter()
|
||||||
|
.map(move |x| Suggestion {
|
||||||
|
value: String::from_utf8_lossy(&x).to_string(),
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let prefix = working_set.get_span_contents(span);
|
||||||
|
let prefix = String::from_utf8_lossy(prefix).to_string();
|
||||||
|
let results = if find_externals {
|
||||||
|
let results_external =
|
||||||
|
self.external_command_completion(&prefix)
|
||||||
|
.into_iter()
|
||||||
|
.map(move |x| Suggestion {
|
||||||
|
value: x,
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for external in results_external {
|
||||||
|
if results.contains(&external) {
|
||||||
|
results.push(Suggestion {
|
||||||
|
value: format!("^{}", external.value),
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: external.span,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results.push(external)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
} else {
|
||||||
|
results
|
||||||
|
};
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for CommandCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
pos: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||||
|
let last = self
|
||||||
|
.flattened
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.skip_while(|x| x.0.end > pos)
|
||||||
|
.take_while(|x| {
|
||||||
|
matches!(
|
||||||
|
x.1,
|
||||||
|
FlatShape::InternalCall
|
||||||
|
| FlatShape::External
|
||||||
|
| FlatShape::ExternalArg
|
||||||
|
| FlatShape::Literal
|
||||||
|
| FlatShape::String
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.last();
|
||||||
|
|
||||||
|
// Options
|
||||||
|
let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance);
|
||||||
|
|
||||||
|
// The last item here would be the earliest shape that could possible by part of this subcommand
|
||||||
|
let subcommands = if let Some(last) = last {
|
||||||
|
self.complete_commands(
|
||||||
|
working_set,
|
||||||
|
Span {
|
||||||
|
start: last.0.start,
|
||||||
|
end: pos,
|
||||||
|
},
|
||||||
|
offset,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
if !subcommands.is_empty() {
|
||||||
|
return (subcommands, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|
||||||
|
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall)
|
||||||
|
|| ((span.end - span.start) == 0)
|
||||||
|
{
|
||||||
|
// we're in a gap or at a command
|
||||||
|
self.complete_commands(working_set, span, offset, true)
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
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 preceding_byte = if span.start > offset {
|
||||||
|
working_set
|
||||||
|
.get_span_contents(Span {
|
||||||
|
start: span.start - 1,
|
||||||
|
end: span.start,
|
||||||
|
})
|
||||||
|
.to_vec()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
// let prefix = working_set.get_span_contents(flat.0);
|
||||||
|
let prefix = String::from_utf8_lossy(&prefix).to_string();
|
||||||
|
let output = file_path_completion(span, &prefix, &cwd)
|
||||||
|
.into_iter()
|
||||||
|
.map(move |x| {
|
||||||
|
if self.flat_idx == 0 {
|
||||||
|
// We're in the command position
|
||||||
|
if x.1.starts_with('"') && !matches!(preceding_byte.get(0), Some(b'^')) {
|
||||||
|
let trimmed = trim_quotes(x.1.as_bytes());
|
||||||
|
let trimmed = String::from_utf8_lossy(trimmed).to_string();
|
||||||
|
let expanded = nu_path::canonicalize_with(trimmed, &cwd);
|
||||||
|
|
||||||
|
if let Ok(expanded) = expanded {
|
||||||
|
if is_executable::is_executable(expanded) {
|
||||||
|
(x.0, format!("^{}", x.1))
|
||||||
|
} else {
|
||||||
|
(x.0, x.1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(x.0, x.1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(x.0, x.1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(x.0, x.1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |x| Suggestion {
|
||||||
|
value: x.1,
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: x.0.start - offset,
|
||||||
|
end: x.0.end - offset,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.chain(subcommands.into_iter())
|
||||||
|
.chain(commands.into_iter())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
(output, options)
|
||||||
|
}
|
||||||
|
}
|
173
crates/nu-cli/src/completions/completer.rs
Normal file
173
crates/nu-cli/src/completions/completer.rs
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
use crate::completions::{
|
||||||
|
CommandCompletion, Completer, CustomCompletion, FileCompletion, FlagCompletion,
|
||||||
|
VariableCompletion,
|
||||||
|
};
|
||||||
|
use nu_parser::{flatten_expression, parse, FlatShape};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
Span, Value,
|
||||||
|
};
|
||||||
|
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NuCompleter {
|
||||||
|
engine_state: EngineState,
|
||||||
|
stack: Stack,
|
||||||
|
config: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NuCompleter {
|
||||||
|
pub fn new(engine_state: EngineState, stack: Stack, config: Option<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the completion for a given completer
|
||||||
|
fn process_completion<T: Completer>(
|
||||||
|
&self,
|
||||||
|
completer: &mut T,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
new_span: Span,
|
||||||
|
offset: usize,
|
||||||
|
pos: usize,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
// Fetch
|
||||||
|
let (mut suggestions, options) =
|
||||||
|
completer.fetch(working_set, prefix.clone(), new_span, offset, pos);
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
suggestions = completer.filter(prefix.clone(), suggestions, options.clone());
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
suggestions = completer.sort(suggestions, prefix, options);
|
||||||
|
|
||||||
|
suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
|
let offset = working_set.next_span_start();
|
||||||
|
let mut line = line.to_string();
|
||||||
|
line.insert(pos, 'a');
|
||||||
|
let pos = offset + pos;
|
||||||
|
let (output, _err) = parse(
|
||||||
|
&mut working_set,
|
||||||
|
Some("completer"),
|
||||||
|
line.as_bytes(),
|
||||||
|
false,
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
for pipeline in output.pipelines.into_iter() {
|
||||||
|
for expr in pipeline.expressions {
|
||||||
|
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
||||||
|
|
||||||
|
for (flat_idx, flat) in flattened.iter().enumerate() {
|
||||||
|
if pos >= flat.0.start && pos < flat.0.end {
|
||||||
|
// Create a new span
|
||||||
|
let new_span = Span {
|
||||||
|
start: flat.0.start,
|
||||||
|
end: flat.0.end - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses the prefix
|
||||||
|
let mut prefix = working_set.get_span_contents(flat.0).to_vec();
|
||||||
|
prefix.remove(pos - flat.0.start);
|
||||||
|
|
||||||
|
// Variables completion
|
||||||
|
if prefix.starts_with(b"$") {
|
||||||
|
let mut completer = VariableCompletion::new(self.engine_state.clone());
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags completion
|
||||||
|
if prefix.starts_with(b"-") {
|
||||||
|
let mut completer = FlagCompletion::new(expr);
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match other types
|
||||||
|
match &flat.1 {
|
||||||
|
FlatShape::Custom(decl_id) => {
|
||||||
|
let mut completer = CustomCompletion::new(
|
||||||
|
self.engine_state.clone(),
|
||||||
|
self.stack.clone(),
|
||||||
|
self.config.clone(),
|
||||||
|
*decl_id,
|
||||||
|
line,
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FlatShape::Filepath | FlatShape::GlobPattern => {
|
||||||
|
let mut completer = FileCompletion::new(self.engine_state.clone());
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
flat_shape => {
|
||||||
|
let mut completer = CommandCompletion::new(
|
||||||
|
self.engine_state.clone(),
|
||||||
|
&working_set,
|
||||||
|
flattened.clone(),
|
||||||
|
flat_idx,
|
||||||
|
flat_shape.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.process_completion(
|
||||||
|
&mut completer,
|
||||||
|
&working_set,
|
||||||
|
prefix,
|
||||||
|
new_span,
|
||||||
|
offset,
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReedlineCompleter for NuCompleter {
|
||||||
|
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
|
self.completion_helper(line, pos)
|
||||||
|
}
|
||||||
|
}
|
33
crates/nu-cli/src/completions/completion_options.rs
Normal file
33
crates/nu-cli/src/completions/completion_options.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SortBy {
|
||||||
|
LevenshteinDistance,
|
||||||
|
Ascending,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompletionOptions {
|
||||||
|
pub case_sensitive: bool,
|
||||||
|
pub positional: bool,
|
||||||
|
pub sort_by: SortBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionOptions {
|
||||||
|
pub fn new(case_sensitive: bool, positional: bool, sort_by: SortBy) -> Self {
|
||||||
|
Self {
|
||||||
|
case_sensitive,
|
||||||
|
positional,
|
||||||
|
sort_by,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CompletionOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
case_sensitive: true,
|
||||||
|
positional: true,
|
||||||
|
sort_by: SortBy::Ascending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
171
crates/nu-cli/src/completions/custom_completions.rs
Normal file
171
crates/nu-cli/src/completions/custom_completions.rs
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
use crate::completions::{Completer, CompletionOptions, SortBy};
|
||||||
|
use nu_engine::eval_call;
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::{Call, Expr, Expression},
|
||||||
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
|
PipelineData, Span, Type, Value, CONFIG_VARIABLE_ID,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
pub struct CustomCompletion {
|
||||||
|
engine_state: EngineState,
|
||||||
|
stack: Stack,
|
||||||
|
config: Option<Value>,
|
||||||
|
decl_id: usize,
|
||||||
|
line: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomCompletion {
|
||||||
|
pub fn new(
|
||||||
|
engine_state: EngineState,
|
||||||
|
stack: Stack,
|
||||||
|
config: Option<Value>,
|
||||||
|
decl_id: usize,
|
||||||
|
line: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
config,
|
||||||
|
decl_id,
|
||||||
|
line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_completions<'a>(
|
||||||
|
&self,
|
||||||
|
list: impl Iterator<Item = &'a Value>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
list.filter_map(move |x| {
|
||||||
|
let s = x.as_string();
|
||||||
|
|
||||||
|
match s {
|
||||||
|
Ok(s) => Some(Suggestion {
|
||||||
|
value: s,
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for CustomCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
_: &StateWorkingSet,
|
||||||
|
_: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
pos: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||||
|
// Line position
|
||||||
|
let line_pos = pos - offset;
|
||||||
|
|
||||||
|
// Set up our initial config to start from
|
||||||
|
if let Some(conf) = &self.config {
|
||||||
|
self.stack.vars.insert(CONFIG_VARIABLE_ID, conf.clone());
|
||||||
|
} else {
|
||||||
|
self.stack.vars.insert(
|
||||||
|
CONFIG_VARIABLE_ID,
|
||||||
|
Value::Record {
|
||||||
|
cols: vec![],
|
||||||
|
vals: vec![],
|
||||||
|
span: Span { start: 0, end: 0 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call custom declaration
|
||||||
|
let result = eval_call(
|
||||||
|
&self.engine_state,
|
||||||
|
&mut self.stack,
|
||||||
|
&Call {
|
||||||
|
decl_id: self.decl_id,
|
||||||
|
head: span,
|
||||||
|
positional: vec![
|
||||||
|
Expression {
|
||||||
|
span: Span { start: 0, end: 0 },
|
||||||
|
ty: Type::String,
|
||||||
|
expr: Expr::String(self.line.clone()),
|
||||||
|
custom_completion: None,
|
||||||
|
},
|
||||||
|
Expression {
|
||||||
|
span: Span { start: 0, end: 0 },
|
||||||
|
ty: Type::Int,
|
||||||
|
expr: Expr::Int(line_pos as i64),
|
||||||
|
custom_completion: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
named: vec![],
|
||||||
|
redirect_stdout: true,
|
||||||
|
redirect_stderr: true,
|
||||||
|
},
|
||||||
|
PipelineData::new(span),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse result
|
||||||
|
let (suggestions, options) = match result {
|
||||||
|
Ok(pd) => {
|
||||||
|
let value = pd.into_value(span);
|
||||||
|
match &value {
|
||||||
|
Value::Record { .. } => {
|
||||||
|
let completions = value
|
||||||
|
.get_data_by_key("completions")
|
||||||
|
.and_then(|val| {
|
||||||
|
val.as_list()
|
||||||
|
.ok()
|
||||||
|
.map(|it| self.map_completions(it.iter(), span, offset))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let options = value.get_data_by_key("options");
|
||||||
|
|
||||||
|
let options = if let Some(Value::Record { .. }) = &options {
|
||||||
|
let options = options.unwrap_or_default();
|
||||||
|
let should_sort = options
|
||||||
|
.get_data_by_key("sort")
|
||||||
|
.and_then(|val| val.as_bool().ok())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
CompletionOptions {
|
||||||
|
case_sensitive: options
|
||||||
|
.get_data_by_key("case_sensitive")
|
||||||
|
.and_then(|val| val.as_bool().ok())
|
||||||
|
.unwrap_or(true),
|
||||||
|
positional: options
|
||||||
|
.get_data_by_key("positional")
|
||||||
|
.and_then(|val| val.as_bool().ok())
|
||||||
|
.unwrap_or(true),
|
||||||
|
sort_by: if should_sort {
|
||||||
|
SortBy::Ascending
|
||||||
|
} else {
|
||||||
|
SortBy::None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CompletionOptions::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
(completions, options)
|
||||||
|
}
|
||||||
|
Value::List { vals, .. } => {
|
||||||
|
let completions = self.map_completions(vals.iter(), span, offset);
|
||||||
|
(completions, CompletionOptions::default())
|
||||||
|
}
|
||||||
|
_ => (vec![], CompletionOptions::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (vec![], CompletionOptions::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
(suggestions, options)
|
||||||
|
}
|
||||||
|
}
|
116
crates/nu-cli/src/completions/file_completions.rs
Normal file
116
crates/nu-cli/src/completions/file_completions.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
use crate::completions::{Completer, CompletionOptions, SortBy};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
Span,
|
||||||
|
};
|
||||||
|
use reedline::Suggestion;
|
||||||
|
use std::path::{is_separator, Path};
|
||||||
|
|
||||||
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FileCompletion {
|
||||||
|
engine_state: EngineState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileCompletion {
|
||||||
|
pub fn new(engine_state: EngineState) -> Self {
|
||||||
|
Self { engine_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for FileCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
_: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
_: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||||
|
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 output: Vec<_> = file_path_completion(span, &prefix, &cwd)
|
||||||
|
.into_iter()
|
||||||
|
.map(move |x| Suggestion {
|
||||||
|
value: x.1,
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: x.0.start - offset,
|
||||||
|
end: x.0.end - offset,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Options
|
||||||
|
let options = CompletionOptions::new(true, true, SortBy::LevenshteinDistance);
|
||||||
|
|
||||||
|
(output, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_path_completion(
|
||||||
|
span: nu_protocol::Span,
|
||||||
|
partial: &str,
|
||||||
|
cwd: &str,
|
||||||
|
) -> Vec<(nu_protocol::Span, String)> {
|
||||||
|
let partial = partial.replace('\'', "");
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matches(partial: &str, from: &str) -> bool {
|
||||||
|
from.to_ascii_lowercase()
|
||||||
|
.starts_with(&partial.to_ascii_lowercase())
|
||||||
|
}
|
81
crates/nu-cli/src/completions/flag_completions.rs
Normal file
81
crates/nu-cli/src/completions/flag_completions.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use crate::completions::{Completer, CompletionOptions};
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::{Expr, Expression},
|
||||||
|
engine::StateWorkingSet,
|
||||||
|
Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FlagCompletion {
|
||||||
|
expression: Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagCompletion {
|
||||||
|
pub fn new(expression: Expression) -> Self {
|
||||||
|
Self { expression }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for FlagCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
_: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||||
|
// Check if it's a flag
|
||||||
|
if let Expr::Call(call) = &self.expression.expr {
|
||||||
|
let decl = working_set.get_decl(call.decl_id);
|
||||||
|
let sig = decl.signature();
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
for named in &sig.named {
|
||||||
|
let flag_desc = &named.desc;
|
||||||
|
if let Some(short) = named.short {
|
||||||
|
let mut named = vec![0; short.len_utf8()];
|
||||||
|
short.encode_utf8(&mut named);
|
||||||
|
named.insert(0, b'-');
|
||||||
|
if named.starts_with(&prefix) {
|
||||||
|
output.push(Suggestion {
|
||||||
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
|
description: Some(flag_desc.to_string()),
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if named.long.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut named = named.long.as_bytes().to_vec();
|
||||||
|
named.insert(0, b'-');
|
||||||
|
named.insert(0, b'-');
|
||||||
|
if named.starts_with(&prefix) {
|
||||||
|
output.push(Suggestion {
|
||||||
|
value: String::from_utf8_lossy(&named).to_string(),
|
||||||
|
description: Some(flag_desc.to_string()),
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (output, CompletionOptions::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
(vec![], CompletionOptions::default())
|
||||||
|
}
|
||||||
|
}
|
17
crates/nu-cli/src/completions/mod.rs
Normal file
17
crates/nu-cli/src/completions/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
mod base;
|
||||||
|
mod command_completions;
|
||||||
|
mod completer;
|
||||||
|
mod completion_options;
|
||||||
|
mod custom_completions;
|
||||||
|
mod file_completions;
|
||||||
|
mod flag_completions;
|
||||||
|
mod variable_completions;
|
||||||
|
|
||||||
|
pub use base::Completer;
|
||||||
|
pub use command_completions::CommandCompletion;
|
||||||
|
pub use completer::NuCompleter;
|
||||||
|
pub use completion_options::{CompletionOptions, SortBy};
|
||||||
|
pub use custom_completions::CustomCompletion;
|
||||||
|
pub use file_completions::{file_path_completion, FileCompletion};
|
||||||
|
pub use flag_completions::FlagCompletion;
|
||||||
|
pub use variable_completions::VariableCompletion;
|
82
crates/nu-cli/src/completions/variable_completions.rs
Normal file
82
crates/nu-cli/src/completions/variable_completions.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use crate::completions::{Completer, CompletionOptions};
|
||||||
|
use nu_protocol::{
|
||||||
|
engine::{EngineState, StateWorkingSet},
|
||||||
|
Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
use reedline::Suggestion;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VariableCompletion {
|
||||||
|
engine_state: EngineState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VariableCompletion {
|
||||||
|
pub fn new(engine_state: EngineState) -> Self {
|
||||||
|
Self { engine_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer for VariableCompletion {
|
||||||
|
fn fetch(
|
||||||
|
&mut self,
|
||||||
|
working_set: &StateWorkingSet,
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
_: usize,
|
||||||
|
) -> (Vec<Suggestion>, CompletionOptions) {
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
|
||||||
|
|
||||||
|
for builtin in builtins {
|
||||||
|
if builtin.as_bytes().starts_with(&prefix) {
|
||||||
|
output.push(Suggestion {
|
||||||
|
value: builtin.to_string(),
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for scope in &working_set.delta.scope {
|
||||||
|
for v in &scope.vars {
|
||||||
|
if v.0.starts_with(&prefix) {
|
||||||
|
output.push(Suggestion {
|
||||||
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for scope in &self.engine_state.scope {
|
||||||
|
for v in &scope.vars {
|
||||||
|
if v.0.starts_with(&prefix) {
|
||||||
|
output.push(Suggestion {
|
||||||
|
value: String::from_utf8_lossy(v.0).to_string(),
|
||||||
|
description: None,
|
||||||
|
extra: None,
|
||||||
|
span: reedline::Span {
|
||||||
|
start: span.start - offset,
|
||||||
|
end: span.end - offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.dedup();
|
||||||
|
|
||||||
|
(output, CompletionOptions::default())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::reedline_config::add_menus;
|
use crate::reedline_config::add_menus;
|
||||||
|
use crate::{completions::NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
|
||||||
use crate::{prompt_update, reedline_config};
|
use crate::{prompt_update, reedline_config};
|
||||||
use crate::{
|
use crate::{
|
||||||
reedline_config::KeybindingsMode,
|
reedline_config::KeybindingsMode,
|
||||||
util::{eval_source, report_error},
|
util::{eval_source, report_error},
|
||||||
};
|
};
|
||||||
use crate::{NuCompleter, NuHighlighter, NuValidator, NushellPrompt};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
|
|
@ -3,7 +3,7 @@ use nu_protocol::DeclId;
|
||||||
use nu_protocol::{engine::StateWorkingSet, Span};
|
use nu_protocol::{engine::StateWorkingSet, Span};
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Eq, PartialEq, Ord, Clone, PartialOrd)]
|
||||||
pub enum FlatShape {
|
pub enum FlatShape {
|
||||||
Garbage,
|
Garbage,
|
||||||
Nothing,
|
Nothing,
|
||||||
|
|
Loading…
Reference in a new issue