forked from extern/nushell
Extract completions into subcrate. (#3631)
This commit is contained in:
committed by
GitHub
parent
04c0e94349
commit
7c8fb060f1
@ -1,142 +0,0 @@
|
||||
use std::iter::FromIterator;
|
||||
use std::path::Path;
|
||||
|
||||
use indexmap::set::IndexSet;
|
||||
|
||||
use super::matchers::Matcher;
|
||||
use crate::completion::{Completer, CompletionContext, Suggestion};
|
||||
use nu_engine::EvaluationContext;
|
||||
use nu_test_support::NATIVE_PATH_ENV_VAR;
|
||||
|
||||
pub struct CommandCompleter;
|
||||
|
||||
impl Completer for CommandCompleter {
|
||||
fn complete(
|
||||
&self,
|
||||
ctx: &CompletionContext<'_>,
|
||||
partial: &str,
|
||||
matcher: &dyn Matcher,
|
||||
) -> Vec<Suggestion> {
|
||||
let context: &EvaluationContext = ctx.as_ref();
|
||||
let mut commands: IndexSet<String> = IndexSet::from_iter(context.scope.get_command_names());
|
||||
|
||||
// Command suggestions can come from three possible sets:
|
||||
// 1. internal command names,
|
||||
// 2. external command names relative to PATH env var, and
|
||||
// 3. any other executable (that matches what's been typed so far).
|
||||
|
||||
let path_executables = find_path_executables().unwrap_or_default();
|
||||
|
||||
// TODO quote these, if necessary
|
||||
commands.extend(path_executables.into_iter());
|
||||
|
||||
let mut suggestions: Vec<_> = commands
|
||||
.into_iter()
|
||||
.filter(|v| matcher.matches(partial, v))
|
||||
.map(|v| Suggestion {
|
||||
replacement: v.clone(),
|
||||
display: v,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !partial.is_empty() {
|
||||
let path_completer = crate::completion::path::PathCompleter;
|
||||
let path_results = path_completer.path_suggestions(partial, matcher);
|
||||
let iter = path_results.into_iter().filter_map(|path_suggestion| {
|
||||
let path = path_suggestion.path;
|
||||
if path.is_dir() || is_executable(&path) {
|
||||
Some(path_suggestion.suggestion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
suggestions.extend(iter);
|
||||
}
|
||||
|
||||
suggestions
|
||||
}
|
||||
}
|
||||
|
||||
// TODO create a struct for "is executable" and store this information in it so we don't recompute
|
||||
// on every dir entry
|
||||
|
||||
#[cfg(windows)]
|
||||
fn pathext() -> Option<Vec<String>> {
|
||||
std::env::var_os("PATHEXT").map(|v| {
|
||||
v.to_string_lossy()
|
||||
.split(';')
|
||||
// Filter out empty tokens and ';' at the end
|
||||
.filter(|f| f.len() > 1)
|
||||
// Cut off the leading '.' character
|
||||
.map(|ext| ext[1..].to_string())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_executable(path: &Path) -> bool {
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
let file_type = metadata.file_type();
|
||||
|
||||
// If the entry isn't a file, it cannot be executable
|
||||
if !(file_type.is_file() || file_type.is_symlink()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(extension) = path.extension() {
|
||||
if let Some(exts) = pathext() {
|
||||
exts.iter()
|
||||
.any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn is_executable(_path: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn is_executable(path: &Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
if let Ok(metadata) = path.metadata() {
|
||||
let filetype = metadata.file_type();
|
||||
let permissions = metadata.permissions();
|
||||
|
||||
// The file is executable if it is a directory or a symlink and the permissions are set for
|
||||
// owner, group, or other
|
||||
(filetype.is_file() || filetype.is_symlink()) && (permissions.mode() & 0o111 != 0)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO cache these, but watch for changes to PATH
|
||||
fn find_path_executables() -> Option<IndexSet<String>> {
|
||||
let path_var = std::env::var_os(NATIVE_PATH_ENV_VAR)?;
|
||||
let paths: Vec<_> = std::env::split_paths(&path_var).collect();
|
||||
|
||||
let mut executables: IndexSet<String> = IndexSet::new();
|
||||
for path in paths {
|
||||
if let Ok(mut contents) = std::fs::read_dir(path) {
|
||||
while let Some(Ok(item)) = contents.next() {
|
||||
if is_executable(&item.path()) {
|
||||
if let Ok(name) = item.file_name().into_string() {
|
||||
executables.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(executables)
|
||||
}
|
@ -1,472 +0,0 @@
|
||||
use nu_protocol::hir::*;
|
||||
use nu_source::{Span, Spanned, SpannedItem};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum LocationType {
|
||||
Command,
|
||||
Flag(String), // command name
|
||||
Argument(Option<String>, Option<String>), // command name, argument name
|
||||
Variable,
|
||||
}
|
||||
|
||||
pub type CompletionLocation = Spanned<LocationType>;
|
||||
|
||||
// TODO The below is very similar to shapes / expression_to_flat_shape. Check back October 2020
|
||||
// to see if we're close enough to just make use of those.
|
||||
|
||||
struct Flatten<'s> {
|
||||
line: &'s str,
|
||||
command: Option<String>,
|
||||
flag: Option<String>,
|
||||
}
|
||||
|
||||
impl<'s> Flatten<'s> {
|
||||
/// Converts a SpannedExpression into a completion location for use in NuCompleter
|
||||
fn expression(&self, e: &SpannedExpression) -> Vec<CompletionLocation> {
|
||||
match &e.expr {
|
||||
Expression::Block(block) => self.completion_locations(block),
|
||||
Expression::Subexpression(block) => self.completion_locations(block),
|
||||
Expression::List(exprs) => exprs.iter().flat_map(|v| self.expression(v)).collect(),
|
||||
Expression::Table(headers, cells) => headers
|
||||
.iter()
|
||||
.flat_map(|v| self.expression(v))
|
||||
.chain(
|
||||
cells
|
||||
.iter()
|
||||
.flat_map(|v| v.iter().flat_map(|v| self.expression(v))),
|
||||
)
|
||||
.collect(),
|
||||
Expression::Command => vec![LocationType::Command.spanned(e.span)],
|
||||
Expression::FullColumnPath(path) => self.expression(&path.head),
|
||||
Expression::Variable(_, _) => vec![LocationType::Variable.spanned(e.span)],
|
||||
|
||||
Expression::Boolean(_)
|
||||
| Expression::FilePath(_)
|
||||
| Expression::Literal(Literal::ColumnPath(_))
|
||||
| Expression::Literal(Literal::GlobPattern(_))
|
||||
| Expression::Literal(Literal::Number(_))
|
||||
| Expression::Literal(Literal::Size(_, _))
|
||||
| Expression::Literal(Literal::String(_)) => {
|
||||
vec![
|
||||
LocationType::Argument(self.command.clone(), self.flag.clone()).spanned(e.span),
|
||||
]
|
||||
}
|
||||
|
||||
Expression::Binary(binary) => {
|
||||
let mut result = Vec::new();
|
||||
result.append(&mut self.expression(&binary.left));
|
||||
result.append(&mut self.expression(&binary.right));
|
||||
result
|
||||
}
|
||||
Expression::Range(range) => {
|
||||
let mut result = Vec::new();
|
||||
if let Some(left) = &range.left {
|
||||
result.append(&mut self.expression(left));
|
||||
}
|
||||
if let Some(right) = &range.right {
|
||||
result.append(&mut self.expression(right));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
Expression::ExternalWord
|
||||
| Expression::ExternalCommand(_)
|
||||
| Expression::Synthetic(_)
|
||||
| Expression::Literal(Literal::Operator(_))
|
||||
| Expression::Literal(Literal::Bare(_))
|
||||
| Expression::Garbage => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_command(&self, internal: &InternalCommand) -> Vec<CompletionLocation> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
match internal.args.head.expr {
|
||||
Expression::Command => {
|
||||
result.push(LocationType::Command.spanned(internal.name_span));
|
||||
}
|
||||
Expression::Literal(Literal::String(_)) => {
|
||||
result.push(LocationType::Command.spanned(internal.name_span));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(positionals) = &internal.args.positional {
|
||||
let mut positionals = positionals.iter();
|
||||
|
||||
if internal.name == "run_external" {
|
||||
if let Some(external_command) = positionals.next() {
|
||||
result.push(LocationType::Command.spanned(external_command.span));
|
||||
}
|
||||
}
|
||||
|
||||
result.extend(positionals.flat_map(|positional| match positional.expr {
|
||||
Expression::Garbage => {
|
||||
let garbage = positional.span.slice(self.line);
|
||||
let location = if garbage.starts_with('-') {
|
||||
LocationType::Flag(internal.name.clone())
|
||||
} else {
|
||||
// TODO we may be able to map this to the name of a positional,
|
||||
// but we'll need a signature
|
||||
LocationType::Argument(Some(internal.name.clone()), None)
|
||||
};
|
||||
|
||||
vec![location.spanned(positional.span)]
|
||||
}
|
||||
|
||||
_ => self.expression(positional),
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(named) = &internal.args.named {
|
||||
for (name, kind) in &named.named {
|
||||
match kind {
|
||||
NamedValue::PresentSwitch(span) => {
|
||||
result.push(LocationType::Flag(internal.name.clone()).spanned(*span));
|
||||
}
|
||||
|
||||
NamedValue::Value(span, expr) => {
|
||||
result.push(LocationType::Flag(internal.name.clone()).spanned(*span));
|
||||
result.append(&mut self.with_flag(name.clone()).expression(expr));
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn pipeline(&self, pipeline: &Pipeline) -> Vec<CompletionLocation> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for command in &pipeline.list {
|
||||
match command {
|
||||
ClassifiedCommand::Internal(internal) => {
|
||||
let engine = self.with_command(internal.name.clone());
|
||||
result.append(&mut engine.internal_command(internal));
|
||||
}
|
||||
|
||||
ClassifiedCommand::Expr(expr) => result.append(&mut self.expression(expr)),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Flattens the block into a Vec of completion locations
|
||||
pub fn completion_locations(&self, block: &Block) -> Vec<CompletionLocation> {
|
||||
block
|
||||
.block
|
||||
.iter()
|
||||
.flat_map(|g| g.pipelines.iter().flat_map(|v| self.pipeline(v)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn new(line: &'s str) -> Flatten<'s> {
|
||||
Flatten {
|
||||
line,
|
||||
command: None,
|
||||
flag: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_command(&self, command: String) -> Flatten<'s> {
|
||||
Flatten {
|
||||
line: self.line,
|
||||
command: Some(command),
|
||||
flag: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_flag(&self, flag: String) -> Flatten<'s> {
|
||||
Flatten {
|
||||
line: self.line,
|
||||
command: self.command.clone(),
|
||||
flag: Some(flag),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Characters that precede a command name
|
||||
const BEFORE_COMMAND_CHARS: &[char] = &['|', '(', ';'];
|
||||
|
||||
/// Determines the completion location for a given block at the given cursor position
|
||||
pub fn completion_location(line: &str, block: &Block, pos: usize) -> Vec<CompletionLocation> {
|
||||
let completion_engine = Flatten::new(line);
|
||||
let locations = completion_engine.completion_locations(block);
|
||||
|
||||
if locations.is_empty() {
|
||||
vec![LocationType::Command.spanned(Span::unknown())]
|
||||
} else {
|
||||
let mut command = None;
|
||||
let mut prev = None;
|
||||
|
||||
for loc in &locations {
|
||||
// We don't use span.contains because we want to include the end. This handles the case
|
||||
// where the cursor is just after the text (i.e., no space between cursor and text)
|
||||
if loc.span.start() <= pos && pos <= loc.span.end() {
|
||||
// The parser sees the "-" in `cmd -` as an argument, but the user is likely
|
||||
// expecting a flag.
|
||||
return match loc.item {
|
||||
LocationType::Argument(ref cmd, _) => {
|
||||
if loc.span.slice(line) == "-" {
|
||||
let cmd = cmd.clone();
|
||||
let span = loc.span;
|
||||
vec![
|
||||
loc.clone(),
|
||||
LocationType::Flag(cmd.unwrap_or_default()).spanned(span),
|
||||
]
|
||||
} else {
|
||||
let mut output = vec![];
|
||||
|
||||
for rloc in locations.iter().rev() {
|
||||
if let Spanned {
|
||||
span,
|
||||
item: LocationType::Command,
|
||||
} = &rloc
|
||||
{
|
||||
if span.start() <= pos {
|
||||
output.push(
|
||||
LocationType::Command
|
||||
.spanned(Span::new(span.start(), pos)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.push(loc.clone());
|
||||
output
|
||||
}
|
||||
}
|
||||
_ => vec![loc.clone()],
|
||||
};
|
||||
} else if pos < loc.span.start() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let LocationType::Command = loc.item {
|
||||
command = Some(String::from(loc.span.slice(line)));
|
||||
}
|
||||
|
||||
prev = Some(loc);
|
||||
}
|
||||
|
||||
if let Some(prev) = prev {
|
||||
let mut locations = vec![];
|
||||
// Cursor is between locations (or at the end). Look at the line to see if the cursor
|
||||
// is after some character that would imply we're in the command position.
|
||||
let start = prev.span.end();
|
||||
|
||||
if let Spanned {
|
||||
item: LocationType::Command,
|
||||
span,
|
||||
} = &prev
|
||||
{
|
||||
locations.push(LocationType::Command.spanned(Span::new(span.start(), pos)));
|
||||
}
|
||||
if line[start..pos].contains(BEFORE_COMMAND_CHARS) {
|
||||
locations.push(LocationType::Command.spanned(Span::new(pos, pos)));
|
||||
} else {
|
||||
// TODO this should be able to be mapped to a command
|
||||
locations.push(LocationType::Argument(command, None).spanned(Span::new(pos, pos)));
|
||||
}
|
||||
locations
|
||||
} else {
|
||||
// Cursor is before any possible completion location, so must be a command
|
||||
vec![LocationType::Command.spanned(Span::unknown())]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
|
||||
use nu_parser::{classify_block, lex, parse_block, ParserScope};
|
||||
use nu_protocol::{Signature, SyntaxShape};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct VecRegistry(Vec<Signature>);
|
||||
|
||||
impl From<Vec<Signature>> for VecRegistry {
|
||||
fn from(v: Vec<Signature>) -> Self {
|
||||
VecRegistry(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl ParserScope for VecRegistry {
|
||||
fn has_signature(&self, name: &str) -> bool {
|
||||
self.0.iter().any(|v| v.name == name)
|
||||
}
|
||||
|
||||
fn get_signature(&self, name: &str) -> Option<nu_protocol::Signature> {
|
||||
self.0.iter().find(|v| v.name == name).map(Clone::clone)
|
||||
}
|
||||
|
||||
fn get_alias(&self, _name: &str) -> Option<Vec<Spanned<String>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn add_alias(&self, _name: &str, _replacement: Vec<Spanned<String>>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn add_definition(&self, _block: Arc<Block>) {}
|
||||
|
||||
fn get_definitions(&self) -> Vec<Arc<Block>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn enter_scope(&self) {}
|
||||
|
||||
fn exit_scope(&self) {}
|
||||
}
|
||||
|
||||
mod completion_location {
|
||||
use super::*;
|
||||
|
||||
use nu_parser::ParserScope;
|
||||
|
||||
fn completion_location(
|
||||
line: &str,
|
||||
scope: &dyn ParserScope,
|
||||
pos: usize,
|
||||
) -> Vec<LocationType> {
|
||||
let (tokens, _) = lex(line, 0);
|
||||
let (lite_block, _) = parse_block(tokens);
|
||||
|
||||
scope.enter_scope();
|
||||
let (block, _) = classify_block(&lite_block, scope);
|
||||
scope.exit_scope();
|
||||
|
||||
super::completion_location(line, &block, pos)
|
||||
.into_iter()
|
||||
.map(|v| v.item)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_internal_command_names() {
|
||||
let registry: VecRegistry =
|
||||
vec![Signature::build("echo").rest(SyntaxShape::Any, "the values to echo")].into();
|
||||
let line = "echo 1 | echo 2";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 10),
|
||||
vec![LocationType::Command],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_external_command_names() {
|
||||
let registry: VecRegistry = Vec::new().into();
|
||||
let line = "echo 1 | echo 2";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 10),
|
||||
vec![LocationType::Command],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_command_names_when_cursor_immediately_after_command_name() {
|
||||
let registry: VecRegistry = Vec::new().into();
|
||||
let line = "echo 1 | echo 2";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 4),
|
||||
vec![LocationType::Command],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_variables() {
|
||||
let registry: VecRegistry = Vec::new().into();
|
||||
let line = "echo $nu.env.";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 13),
|
||||
vec![LocationType::Variable],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_flags() {
|
||||
let registry: VecRegistry = vec![Signature::build("du")
|
||||
.switch("recursive", "the values to echo", None)
|
||||
.rest(SyntaxShape::Any, "blah")]
|
||||
.into();
|
||||
|
||||
let line = "du --recurs";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 7),
|
||||
vec![LocationType::Flag("du".to_string())],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_incomplete_nested_structure() {
|
||||
let registry: VecRegistry = vec![Signature::build("sys")].into();
|
||||
let line = "echo (sy";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 8),
|
||||
vec![LocationType::Command],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_correct_command_name_for_argument() {
|
||||
let registry: VecRegistry = vec![Signature::build("cd")].into();
|
||||
let line = "cd ";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 3),
|
||||
vec![
|
||||
LocationType::Command,
|
||||
LocationType::Argument(Some("cd".to_string()), None)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_flags_with_just_a_single_hyphen() {
|
||||
let registry: VecRegistry = vec![Signature::build("du")
|
||||
.switch("recursive", "the values to echo", None)
|
||||
.rest(SyntaxShape::Any, "blah")]
|
||||
.into();
|
||||
|
||||
let line = "du -";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 3),
|
||||
vec![
|
||||
LocationType::Argument(Some("du".to_string()), None),
|
||||
LocationType::Flag("du".to_string()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_arguments() {
|
||||
let registry: VecRegistry =
|
||||
vec![Signature::build("echo").rest(SyntaxShape::Any, "the values to echo")].into();
|
||||
let line = "echo 1 | echo 2";
|
||||
|
||||
assert_eq!(
|
||||
completion_location(line, ®istry, 6),
|
||||
vec![
|
||||
LocationType::Command,
|
||||
LocationType::Argument(Some("echo".to_string()), None)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
use super::matchers::Matcher;
|
||||
use crate::completion::{Completer, CompletionContext, Suggestion};
|
||||
use nu_engine::EvaluationContext;
|
||||
|
||||
pub struct FlagCompleter {
|
||||
pub(crate) cmd: String,
|
||||
}
|
||||
|
||||
impl Completer for FlagCompleter {
|
||||
fn complete(
|
||||
&self,
|
||||
ctx: &CompletionContext<'_>,
|
||||
partial: &str,
|
||||
matcher: &dyn Matcher,
|
||||
) -> Vec<Suggestion> {
|
||||
let context: &EvaluationContext = ctx.as_ref();
|
||||
|
||||
if let Some(cmd) = context.scope.get_command(&self.cmd) {
|
||||
let sig = cmd.signature();
|
||||
let mut suggestions = Vec::new();
|
||||
for (name, (named_type, _desc)) in sig.named.iter() {
|
||||
suggestions.push(format!("--{}", name));
|
||||
|
||||
if let Some(c) = named_type.get_short() {
|
||||
suggestions.push(format!("-{}", c));
|
||||
}
|
||||
}
|
||||
|
||||
suggestions
|
||||
.into_iter()
|
||||
.filter(|v| matcher.matches(partial, v))
|
||||
.map(|v| Suggestion {
|
||||
replacement: format!("{} ", v),
|
||||
display: v,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
use crate::completion::matchers;
|
||||
pub struct Matcher;
|
||||
|
||||
impl matchers::Matcher for Matcher {
|
||||
fn matches(&self, partial: &str, from: &str) -> bool {
|
||||
from.to_ascii_lowercase()
|
||||
.starts_with(partial.to_ascii_lowercase().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// TODO: check some Unicode matches if this becomes relevant
|
||||
|
||||
// FIXME: could work exhaustively through ['-', '--'. ''] in a loop for each test
|
||||
#[test]
|
||||
fn completes_exact_matches() {
|
||||
let matcher: Box<dyn matchers::Matcher> = Box::new(Matcher);
|
||||
|
||||
assert!(matcher.matches("shouldmatch", "shouldmatch"));
|
||||
assert!(matcher.matches("shouldm", "shouldmatch"));
|
||||
assert!(matcher.matches("--also-should-m", "--also-should-match"));
|
||||
assert!(matcher.matches("-also-should-m", "-also-should-match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_case_insensitive_matches() {
|
||||
let matcher: Box<dyn matchers::Matcher> = Box::new(Matcher);
|
||||
|
||||
assert!(matcher.matches("thisshould", "Thisshouldmatch"));
|
||||
assert!(matcher.matches("--Shouldm", "--shouldmatch"));
|
||||
assert!(matcher.matches("-Shouldm", "-shouldmatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_match_when_unequal() {
|
||||
let matcher: Box<dyn matchers::Matcher> = Box::new(Matcher);
|
||||
|
||||
assert!(!matcher.matches("ashouldmatch", "Shouldnotmatch"));
|
||||
assert!(!matcher.matches("--ashouldnotmatch", "--Shouldnotmatch"));
|
||||
assert!(!matcher.matches("-ashouldnotmatch", "-Shouldnotmatch"));
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use crate::completion::matchers;
|
||||
|
||||
pub struct Matcher;
|
||||
|
||||
impl matchers::Matcher for Matcher {
|
||||
fn matches(&self, partial: &str, from: &str) -> bool {
|
||||
from.starts_with(partial)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn completes_case_sensitive() {
|
||||
let matcher: Box<dyn matchers::Matcher> = Box::new(Matcher);
|
||||
|
||||
//Should match
|
||||
assert!(matcher.matches("shouldmatch", "shouldmatch"));
|
||||
assert!(matcher.matches("shouldm", "shouldmatch"));
|
||||
assert!(matcher.matches("--also-should-m", "--also-should-match"));
|
||||
assert!(matcher.matches("-also-should-m", "-also-should-match"));
|
||||
|
||||
// Should not match
|
||||
assert!(!matcher.matches("--Shouldnot", "--shouldnotmatch"));
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
pub(crate) mod case_insensitive;
|
||||
pub(crate) mod case_sensitive;
|
||||
|
||||
pub trait Matcher {
|
||||
fn matches(&self, partial: &str, from: &str) -> bool;
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
pub(crate) mod command;
|
||||
pub(crate) mod engine;
|
||||
pub(crate) mod flag;
|
||||
pub(crate) mod matchers;
|
||||
pub(crate) mod path;
|
||||
|
||||
use matchers::Matcher;
|
||||
use nu_engine::EvaluationContext;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Suggestion {
|
||||
pub display: String,
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
pub struct CompletionContext<'a>(&'a EvaluationContext);
|
||||
|
||||
impl<'a> CompletionContext<'a> {
|
||||
pub fn new(a: &'a EvaluationContext) -> CompletionContext<'a> {
|
||||
CompletionContext(a)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<EvaluationContext> for CompletionContext<'a> {
|
||||
fn as_ref(&self) -> &EvaluationContext {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Completer {
|
||||
fn complete(
|
||||
&self,
|
||||
ctx: &CompletionContext<'_>,
|
||||
partial: &str,
|
||||
matcher: &dyn Matcher,
|
||||
) -> Vec<Suggestion>;
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::matchers::Matcher;
|
||||
use crate::completion::{Completer, CompletionContext, Suggestion};
|
||||
|
||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||
|
||||
pub struct PathCompleter;
|
||||
|
||||
pub struct PathSuggestion {
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) suggestion: Suggestion,
|
||||
}
|
||||
|
||||
impl PathCompleter {
|
||||
pub fn path_suggestions(&self, partial: &str, matcher: &dyn Matcher) -> Vec<PathSuggestion> {
|
||||
let expanded = nu_parser::expand_ndots(partial);
|
||||
let expanded = expanded.replace(std::path::is_separator, &SEP.to_string());
|
||||
let expanded: &str = expanded.as_ref();
|
||||
|
||||
let (base_dir_name, partial) = match expanded.rfind(SEP) {
|
||||
Some(pos) => expanded.split_at(pos + SEP.len_utf8()),
|
||||
None => ("", expanded),
|
||||
};
|
||||
|
||||
let base_dir = if base_dir_name.is_empty() {
|
||||
PathBuf::from(".")
|
||||
} else {
|
||||
#[cfg(feature = "directories")]
|
||||
{
|
||||
let home_prefix = format!("~{}", SEP);
|
||||
if base_dir_name.starts_with(&home_prefix) {
|
||||
let mut home_dir = dirs_next::home_dir().unwrap_or_else(|| PathBuf::from("~"));
|
||||
home_dir.push(&base_dir_name[2..]);
|
||||
home_dir
|
||||
} else {
|
||||
PathBuf::from(base_dir_name)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "directories"))]
|
||||
{
|
||||
PathBuf::from(base_dir_name)
|
||||
}
|
||||
};
|
||||
|
||||
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 matcher.matches(partial, file_name.as_str()) {
|
||||
let mut path = format!("{}{}", base_dir_name, file_name);
|
||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
path.push(std::path::MAIN_SEPARATOR);
|
||||
file_name.push(std::path::MAIN_SEPARATOR);
|
||||
}
|
||||
|
||||
Some(PathSuggestion {
|
||||
path: entry.path(),
|
||||
suggestion: Suggestion {
|
||||
replacement: path,
|
||||
display: file_name,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for PathCompleter {
|
||||
fn complete(
|
||||
&self,
|
||||
_ctx: &CompletionContext<'_>,
|
||||
partial: &str,
|
||||
matcher: &dyn Matcher,
|
||||
) -> Vec<Suggestion> {
|
||||
self.path_suggestions(partial, matcher)
|
||||
.into_iter()
|
||||
.map(|ps| ps.suggestion)
|
||||
.collect()
|
||||
}
|
||||
}
|
@ -11,8 +11,6 @@ extern crate quickcheck_macros;
|
||||
|
||||
mod app;
|
||||
mod cli;
|
||||
#[cfg(feature = "rustyline-support")]
|
||||
mod completion;
|
||||
mod format;
|
||||
#[cfg(feature = "rustyline-support")]
|
||||
mod keybinding;
|
||||
|
@ -1,7 +1,5 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
|
||||
#[cfg(feature = "rustyline-support")]
|
||||
pub(crate) mod completer;
|
||||
#[cfg(feature = "rustyline-support")]
|
||||
pub(crate) mod helper;
|
||||
|
||||
|
@ -1,179 +0,0 @@
|
||||
use crate::completion::command::CommandCompleter;
|
||||
use crate::completion::flag::FlagCompleter;
|
||||
use crate::completion::matchers;
|
||||
use crate::completion::matchers::Matcher;
|
||||
use crate::completion::path::{PathCompleter, PathSuggestion};
|
||||
use crate::completion::{self, Completer, Suggestion};
|
||||
use nu_engine::EvaluationContext;
|
||||
use nu_parser::ParserScope;
|
||||
use nu_source::{Span, Tag};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) struct NuCompleter {}
|
||||
|
||||
impl NuCompleter {}
|
||||
|
||||
impl NuCompleter {
|
||||
pub fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
context: &completion::CompletionContext,
|
||||
) -> (usize, Vec<Suggestion>) {
|
||||
use completion::engine::LocationType;
|
||||
|
||||
let nu_context: &EvaluationContext = context.as_ref();
|
||||
|
||||
nu_context.scope.enter_scope();
|
||||
let (block, _) = nu_parser::parse(line, 0, &nu_context.scope);
|
||||
nu_context.scope.exit_scope();
|
||||
|
||||
let locations = completion::engine::completion_location(line, &block, pos);
|
||||
|
||||
let matcher = nu_data::config::config(Tag::unknown())
|
||||
.ok()
|
||||
.and_then(|cfg| cfg.get("line_editor").cloned())
|
||||
.and_then(|le| {
|
||||
le.row_entries()
|
||||
.find(|(idx, _value)| idx.as_str() == "completion_match_method")
|
||||
.and_then(|(_idx, value)| value.as_string().ok())
|
||||
})
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
let matcher = matcher.as_str();
|
||||
let matcher: &dyn Matcher = match matcher {
|
||||
"case-insensitive" => &matchers::case_insensitive::Matcher,
|
||||
"case-sensitive" => &matchers::case_sensitive::Matcher,
|
||||
#[cfg(target_os = "windows")]
|
||||
_ => &matchers::case_insensitive::Matcher,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
_ => &matchers::case_sensitive::Matcher,
|
||||
};
|
||||
|
||||
if locations.is_empty() {
|
||||
(pos, Vec::new())
|
||||
} else {
|
||||
let mut pos = locations[0].span.start();
|
||||
|
||||
for location in &locations {
|
||||
if location.span.start() < pos {
|
||||
pos = location.span.start();
|
||||
}
|
||||
}
|
||||
|
||||
let suggestions = locations
|
||||
.into_iter()
|
||||
.flat_map(|location| {
|
||||
let partial = location.span.slice(line).to_string();
|
||||
match location.item {
|
||||
LocationType::Command => {
|
||||
let command_completer = CommandCompleter;
|
||||
command_completer.complete(context, &partial, matcher.to_owned())
|
||||
}
|
||||
|
||||
LocationType::Flag(cmd) => {
|
||||
let flag_completer = FlagCompleter { cmd };
|
||||
flag_completer.complete(context, &partial, matcher.to_owned())
|
||||
}
|
||||
|
||||
LocationType::Argument(cmd, _arg_name) => {
|
||||
let path_completer = PathCompleter;
|
||||
let prepend = Span::new(pos, location.span.start()).slice(line);
|
||||
|
||||
const QUOTE_CHARS: &[char] = &['\'', '"'];
|
||||
|
||||
// TODO Find a better way to deal with quote chars. Can the completion
|
||||
// engine relay this back to us? Maybe have two spans: inner and
|
||||
// outer. The former is what we want to complete, the latter what
|
||||
// we'd need to replace.
|
||||
let (quote_char, partial) = if partial.starts_with(QUOTE_CHARS) {
|
||||
let (head, tail) = partial.split_at(1);
|
||||
(Some(head), tail.to_string())
|
||||
} else {
|
||||
(None, partial)
|
||||
};
|
||||
|
||||
let (mut partial, quoted) = if let Some(quote_char) = quote_char {
|
||||
if partial.ends_with(quote_char) {
|
||||
(partial[..partial.len() - 1].to_string(), true)
|
||||
} else {
|
||||
(partial, false)
|
||||
}
|
||||
} else {
|
||||
(partial, false)
|
||||
};
|
||||
|
||||
partial = partial.split('"').collect::<Vec<_>>().join("");
|
||||
let completed_paths =
|
||||
path_completer.path_suggestions(&partial, matcher);
|
||||
match cmd.as_deref().unwrap_or("") {
|
||||
"cd" => select_directory_suggestions(completed_paths),
|
||||
_ => completed_paths,
|
||||
}
|
||||
.into_iter()
|
||||
.map(|s| Suggestion {
|
||||
replacement: format!(
|
||||
"{}{}",
|
||||
prepend,
|
||||
requote(s.suggestion.replacement, quoted)
|
||||
),
|
||||
display: s.suggestion.display,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
LocationType::Variable => Vec::new(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(pos, suggestions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_directory_suggestions(completed_paths: Vec<PathSuggestion>) -> Vec<PathSuggestion> {
|
||||
completed_paths
|
||||
.into_iter()
|
||||
.filter(|suggestion| {
|
||||
suggestion
|
||||
.path
|
||||
.metadata()
|
||||
.map(|md| md.is_dir())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn requote(orig_value: String, previously_quoted: bool) -> String {
|
||||
let value: Cow<str> = rustyline::completion::unescape(&orig_value, Some('\\'));
|
||||
|
||||
let mut quotes = vec!['"', '\''];
|
||||
let mut should_quote = false;
|
||||
for c in value.chars() {
|
||||
if c.is_whitespace() || c == '#' {
|
||||
should_quote = true;
|
||||
} else if let Some(index) = quotes.iter().position(|q| *q == c) {
|
||||
should_quote = true;
|
||||
quotes.swap_remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
if should_quote {
|
||||
if quotes.is_empty() {
|
||||
// TODO we don't really have an escape character, so there isn't a great option right
|
||||
// now. One possibility is `{{(char backtick)}}`
|
||||
value.to_string()
|
||||
} else {
|
||||
let quote = quotes[0];
|
||||
if previously_quoted {
|
||||
format!("{}{}", quote, value)
|
||||
} else {
|
||||
format!("{}{}{}", quote, value, quote)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
use crate::completion;
|
||||
use crate::shell::completer::NuCompleter;
|
||||
use nu_ansi_term::Color;
|
||||
use nu_completion::NuCompleter;
|
||||
use nu_engine::{DefaultPalette, EvaluationContext, Painter};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use std::borrow::Cow::{self, Owned};
|
||||
@ -28,18 +27,28 @@ impl Helper {
|
||||
}
|
||||
}
|
||||
|
||||
impl rustyline::completion::Candidate for completion::Suggestion {
|
||||
struct CompletionContext<'a>(&'a EvaluationContext);
|
||||
|
||||
impl<'a> nu_completion::CompletionContext for CompletionContext<'a> {
|
||||
fn signature_registry(&self) -> &dyn nu_parser::ParserScope {
|
||||
&self.0.scope
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompletionSuggestion(nu_completion::Suggestion);
|
||||
|
||||
impl rustyline::completion::Candidate for CompletionSuggestion {
|
||||
fn display(&self) -> &str {
|
||||
&self.display
|
||||
&self.0.display
|
||||
}
|
||||
|
||||
fn replacement(&self) -> &str {
|
||||
&self.replacement
|
||||
&self.0.replacement
|
||||
}
|
||||
}
|
||||
|
||||
impl rustyline::completion::Completer for Helper {
|
||||
type Candidate = completion::Suggestion;
|
||||
type Candidate = CompletionSuggestion;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
@ -47,8 +56,10 @@ impl rustyline::completion::Completer for Helper {
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> Result<(usize, Vec<Self::Candidate>), rustyline::error::ReadlineError> {
|
||||
let ctx = completion::CompletionContext::new(&self.context);
|
||||
Ok(self.completer.complete(line, pos, &ctx))
|
||||
let ctx = CompletionContext(&self.context);
|
||||
let (position, suggestions) = self.completer.complete(line, pos, &ctx);
|
||||
let suggestions = suggestions.into_iter().map(CompletionSuggestion).collect();
|
||||
Ok((position, suggestions))
|
||||
}
|
||||
|
||||
fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) {
|
||||
|
Reference in New Issue
Block a user