forked from extern/nushell
Add method to convert ClassifiedBlock into completion locations. (#2316)
The completion engine maps completion locations to spans on a line, which indicate whther to complete a command name, flag name, argument, and so on. Initial implementation is simplistic, with some rough edges, since it relies heavily on the parser's interpretation. For example du - if asking for completions, `-` is considered a positional argument by the parser, but the user is likely looking for a flag. These scenarios will be addressed in a series of progressive enhancements to the engine.
This commit is contained in:
parent
0dd1403a69
commit
9f85b10fcb
@ -7,7 +7,6 @@ use crate::context::Context;
|
|||||||
use crate::git::current_branch;
|
use crate::git::current_branch;
|
||||||
use crate::path::canonicalize;
|
use crate::path::canonicalize;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::shell::completer::NuCompleter;
|
|
||||||
use crate::shell::Helper;
|
use crate::shell::Helper;
|
||||||
use crate::EnvironmentSyncer;
|
use crate::EnvironmentSyncer;
|
||||||
use futures_codec::FramedRead;
|
use futures_codec::FramedRead;
|
||||||
@ -787,10 +786,7 @@ pub async fn cli(
|
|||||||
|
|
||||||
let cwd = context.shell_manager.path();
|
let cwd = context.shell_manager.path();
|
||||||
|
|
||||||
rl.set_helper(Some(crate::shell::Helper::new(
|
rl.set_helper(Some(crate::shell::Helper::new(context.clone())));
|
||||||
Box::new(<NuCompleter as Default>::default()),
|
|
||||||
context.clone(),
|
|
||||||
)));
|
|
||||||
|
|
||||||
let colored_prompt = {
|
let colored_prompt = {
|
||||||
if let Some(prompt) = config.get("prompt") {
|
if let Some(prompt) = config.get("prompt") {
|
||||||
|
115
crates/nu-cli/src/completion/command.rs
Normal file
115
crates/nu-cli/src/completion/command.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
use std::fs::{read_dir, DirEntry};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[cfg(all(windows, feature = "ichwh"))]
|
||||||
|
use ichwh::{IchwhError, IchwhResult};
|
||||||
|
use indexmap::set::IndexSet;
|
||||||
|
|
||||||
|
use crate::completion::{Context, Suggestion};
|
||||||
|
use crate::context;
|
||||||
|
|
||||||
|
pub struct Completer;
|
||||||
|
|
||||||
|
impl Completer {
|
||||||
|
pub fn complete(&self, ctx: &Context<'_>, partial: &str) -> Vec<Suggestion> {
|
||||||
|
let context: &context::Context = ctx.as_ref();
|
||||||
|
let mut commands: IndexSet<String> = IndexSet::from_iter(context.registry.names());
|
||||||
|
|
||||||
|
let path_executables = find_path_executables().unwrap_or_default();
|
||||||
|
|
||||||
|
// TODO quote these, if necessary
|
||||||
|
commands.extend(path_executables.into_iter());
|
||||||
|
|
||||||
|
commands
|
||||||
|
.into_iter()
|
||||||
|
.filter(|v| v.starts_with(partial))
|
||||||
|
.map(|v| Suggestion {
|
||||||
|
replacement: format!("{} ", v),
|
||||||
|
display: v,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These is_executable/pathext implementations are copied from ichwh and modified
|
||||||
|
// to not be async
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn pathext() -> IchwhResult<Vec<String>> {
|
||||||
|
Ok(std::env::var_os("PATHEXT")
|
||||||
|
.ok_or(IchwhError::PathextNotDefined)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.split(';')
|
||||||
|
// Cut off the leading '.' character
|
||||||
|
.map(|ext| ext[1..].to_string())
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_executable(file: &DirEntry) -> bool {
|
||||||
|
if let Ok(metadata) = file.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) = file.path().extension() {
|
||||||
|
if let Ok(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(_file: &DirEntry) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn is_executable(file: &DirEntry) -> bool {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let metadata = file.metadata();
|
||||||
|
|
||||||
|
if let Ok(metadata) = 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("PATH")?;
|
||||||
|
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) = read_dir(path) {
|
||||||
|
while let Some(Ok(item)) = contents.next() {
|
||||||
|
if is_executable(&item) {
|
||||||
|
if let Ok(name) = item.file_name().into_string() {
|
||||||
|
executables.insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(executables)
|
||||||
|
}
|
334
crates/nu-cli/src/completion/engine.rs
Normal file
334
crates/nu-cli/src/completion/engine.rs
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
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::Invocation(block) => self.completion_locations(block),
|
||||||
|
Expression::List(exprs) => exprs.iter().flat_map(|v| self.expression(v)).collect(),
|
||||||
|
Expression::Command(span) => vec![LocationType::Command.spanned(*span)],
|
||||||
|
Expression::Path(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();
|
||||||
|
result.append(&mut self.expression(&range.left));
|
||||||
|
result.append(&mut self.expression(&range.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: &Commands) -> 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(|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) -> Option<CompletionLocation> {
|
||||||
|
let completion_engine = Flatten::new(line);
|
||||||
|
let locations = completion_engine.completion_locations(block);
|
||||||
|
|
||||||
|
if locations.is_empty() {
|
||||||
|
Some(LocationType::Command.spanned(Span::unknown()))
|
||||||
|
} else {
|
||||||
|
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() {
|
||||||
|
return Some(loc);
|
||||||
|
} else if pos < loc.span.start() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = Some(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prev) = prev {
|
||||||
|
// 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 line[start..pos].contains(BEFORE_COMMAND_CHARS) {
|
||||||
|
Some(LocationType::Command.spanned(Span::unknown()))
|
||||||
|
} else {
|
||||||
|
// TODO this should be able to be mapped to a command
|
||||||
|
Some(LocationType::Argument(None, None).spanned(Span::unknown()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Cursor is before any possible completion location, so must be a command
|
||||||
|
Some(LocationType::Command.spanned(Span::unknown()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use nu_parser::SignatureRegistry;
|
||||||
|
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 SignatureRegistry for VecRegistry {
|
||||||
|
fn has(&self, name: &str) -> bool {
|
||||||
|
self.0.iter().any(|v| &v.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, name: &str) -> Option<nu_protocol::Signature> {
|
||||||
|
self.0.iter().find(|v| &v.name == name).map(Clone::clone)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_box(&self) -> Box<dyn SignatureRegistry> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod completion_location {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use nu_parser::{classify_block, lite_parse, SignatureRegistry};
|
||||||
|
|
||||||
|
fn completion_location(
|
||||||
|
line: &str,
|
||||||
|
registry: &dyn SignatureRegistry,
|
||||||
|
pos: usize,
|
||||||
|
) -> Option<LocationType> {
|
||||||
|
let lite_block = lite_parse(line, 0).expect("lite_parse");
|
||||||
|
let block = classify_block(&lite_block, registry);
|
||||||
|
super::completion_location(line, &block.block, pos).map(|v| v.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
Some(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),
|
||||||
|
Some(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),
|
||||||
|
Some(LocationType::Command),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_variables() {
|
||||||
|
let registry: VecRegistry = Vec::new().into();
|
||||||
|
let line = "echo $nu.env.";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
completion_location(line, ®istry, 13),
|
||||||
|
Some(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),
|
||||||
|
Some(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),
|
||||||
|
Some(LocationType::Argument(Some("echo".to_string()), None)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
crates/nu-cli/src/completion/flag.rs
Normal file
33
crates/nu-cli/src/completion/flag.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use crate::completion::{Context, Suggestion};
|
||||||
|
use crate::context;
|
||||||
|
|
||||||
|
pub struct Completer;
|
||||||
|
|
||||||
|
impl Completer {
|
||||||
|
pub fn complete(&self, ctx: &Context<'_>, cmd: String, partial: &str) -> Vec<Suggestion> {
|
||||||
|
let context: &context::Context = ctx.as_ref();
|
||||||
|
|
||||||
|
if let Some(cmd) = context.registry.get_command(&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| v.starts_with(partial))
|
||||||
|
.map(|v| Suggestion {
|
||||||
|
replacement: format!("{} ", v),
|
||||||
|
display: v,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
|
pub(crate) mod command;
|
||||||
|
pub(crate) mod engine;
|
||||||
|
pub(crate) mod flag;
|
||||||
|
pub(crate) mod path;
|
||||||
|
|
||||||
use nu_errors::ShellError;
|
use nu_errors::ShellError;
|
||||||
|
|
||||||
use crate::context;
|
use crate::context;
|
||||||
@ -35,6 +40,4 @@ pub trait Completer {
|
|||||||
pos: usize,
|
pos: usize,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
) -> Result<(usize, Vec<Suggestion>), ShellError>;
|
) -> Result<(usize, Vec<Suggestion>), ShellError>;
|
||||||
|
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String>;
|
|
||||||
}
|
}
|
31
crates/nu-cli/src/completion/path.rs
Normal file
31
crates/nu-cli/src/completion/path.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use rustyline::completion::FilenameCompleter;
|
||||||
|
|
||||||
|
use crate::completion::{Context, Suggestion};
|
||||||
|
|
||||||
|
pub struct Completer {
|
||||||
|
inner: FilenameCompleter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Completer {
|
||||||
|
pub fn new() -> Completer {
|
||||||
|
Completer {
|
||||||
|
inner: FilenameCompleter::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&self, _ctx: &Context<'_>, partial: &str) -> Vec<Suggestion> {
|
||||||
|
let expanded = nu_parser::expand_ndots(partial);
|
||||||
|
|
||||||
|
if let Ok((_pos, pairs)) = self.inner.complete_path(&expanded, expanded.len()) {
|
||||||
|
pairs
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| Suggestion {
|
||||||
|
replacement: v.replacement,
|
||||||
|
display: v.display,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,364 +1,72 @@
|
|||||||
use std::fs::{read_dir, DirEntry};
|
use crate::completion::{self, Suggestion};
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
|
|
||||||
use indexmap::set::IndexSet;
|
|
||||||
use nu_errors::ShellError;
|
|
||||||
use rustyline::completion::{Completer as _, FilenameCompleter};
|
|
||||||
use rustyline::hint::{Hinter as _, HistoryHinter};
|
|
||||||
|
|
||||||
#[cfg(all(windows, feature = "ichwh"))]
|
|
||||||
use ichwh::{IchwhError, IchwhResult};
|
|
||||||
|
|
||||||
use crate::completion::{self, Completer};
|
|
||||||
use crate::context;
|
use crate::context;
|
||||||
use crate::prelude::*;
|
|
||||||
use nu_data::config;
|
|
||||||
|
|
||||||
pub(crate) struct NuCompleter {
|
pub(crate) struct NuCompleter {}
|
||||||
file_completer: FilenameCompleter,
|
|
||||||
hinter: HistoryHinter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
impl NuCompleter {}
|
||||||
enum ReplacementLocation {
|
|
||||||
Command,
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NuCompleter {
|
impl NuCompleter {
|
||||||
fn complete_internal(
|
pub fn complete(
|
||||||
&self,
|
&self,
|
||||||
line: &str,
|
line: &str,
|
||||||
pos: usize,
|
pos: usize,
|
||||||
context: &completion::Context,
|
context: &completion::Context,
|
||||||
) -> rustyline::Result<(usize, Vec<rustyline::completion::Pair>)> {
|
) -> (usize, Vec<Suggestion>) {
|
||||||
let line_chars: Vec<_> = line[..pos].chars().collect();
|
use crate::completion::engine::LocationType;
|
||||||
|
|
||||||
let (replace_pos, replace_loc) = get_replace_pos(line, pos);
|
let nu_context: &context::Context = context.as_ref();
|
||||||
|
let lite_block = match nu_parser::lite_parse(line, 0) {
|
||||||
// See if we're a flag
|
Ok(block) => Some(block),
|
||||||
let mut completions;
|
Err(result) => result.partial,
|
||||||
if pos > 0 && replace_pos < line_chars.len() && line_chars[replace_pos] == '-' {
|
|
||||||
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
|
|
||||||
completions = get_matching_arguments(
|
|
||||||
context.as_ref(),
|
|
||||||
&lite_block,
|
|
||||||
&line_chars,
|
|
||||||
line,
|
|
||||||
replace_pos,
|
|
||||||
pos,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
completions = self.file_completer.complete(line, pos, context.as_ref())?.1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completions = self.file_completer.complete(line, pos, context.as_ref())?.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only complete executables or commands if the thing we're completing
|
|
||||||
// is syntactically a command
|
|
||||||
if replace_loc == ReplacementLocation::Command {
|
|
||||||
let context: &context::Context = context.as_ref();
|
|
||||||
let commands: Vec<String> = context.registry.names();
|
|
||||||
let mut all_executables: IndexSet<_> = commands.iter().map(|x| x.to_string()).collect();
|
|
||||||
|
|
||||||
let complete_from_path = config::config(Tag::unknown())
|
|
||||||
.map(|conf| {
|
|
||||||
conf.get("complete_from_path")
|
|
||||||
.map(|v| v.is_true())
|
|
||||||
.unwrap_or(true)
|
|
||||||
})
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if complete_from_path {
|
|
||||||
let path_executables = find_path_executables().unwrap_or_default();
|
|
||||||
for path_exe in path_executables {
|
|
||||||
all_executables.insert(path_exe);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for exe in all_executables.iter() {
|
let location = lite_block
|
||||||
let mut pos = replace_pos;
|
.map(|block| nu_parser::classify_block(&block, &nu_context.registry))
|
||||||
let mut matched = false;
|
.and_then(|block| {
|
||||||
if pos < line_chars.len() {
|
crate::completion::engine::completion_location(line, &block.block, pos)
|
||||||
for chr in exe.chars() {
|
|
||||||
if line_chars[pos] != chr {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos += 1;
|
|
||||||
if pos == line_chars.len() {
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matched {
|
|
||||||
completions.push(rustyline::completion::Pair {
|
|
||||||
display: exe.to_string(),
|
|
||||||
replacement: exe.to_string(),
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
if let Some(location) = location {
|
||||||
|
let partial = location.span.slice(line);
|
||||||
|
|
||||||
|
let suggestions = match location.item {
|
||||||
|
LocationType::Command => {
|
||||||
|
let command_completer = crate::completion::command::Completer {};
|
||||||
|
command_completer.complete(context, partial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust replacement to deal with a quote already at the cursor. Specifically, if there's
|
LocationType::Flag(cmd) => {
|
||||||
// already a quote at the cursor, but the replacement doesn't have one, we need to ensure
|
let flag_completer = crate::completion::flag::Completer {};
|
||||||
// one exists (to be safe, even if the completion doesn't need it).
|
flag_completer.complete(context, cmd, partial)
|
||||||
for completion in &mut completions {
|
|
||||||
let cursor_char = line.chars().nth(replace_pos);
|
|
||||||
if cursor_char.unwrap_or(' ') == '"' && !completion.replacement.starts_with('"') {
|
|
||||||
completion.replacement.insert(0, '"');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((replace_pos, completions))
|
LocationType::Argument(_cmd, _arg_name) => {
|
||||||
}
|
// TODO use cmd and arg_name to narrow things down further
|
||||||
|
let path_completer = crate::completion::path::Completer::new();
|
||||||
|
path_completer.complete(context, partial)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for NuCompleter {
|
LocationType::Variable => Vec::new(),
|
||||||
fn complete(
|
|
||||||
&self,
|
|
||||||
line: &str,
|
|
||||||
pos: usize,
|
|
||||||
context: &completion::Context,
|
|
||||||
) -> Result<(usize, Vec<completion::Suggestion>), ShellError> {
|
|
||||||
let expanded = nu_parser::expand_ndots(&line);
|
|
||||||
|
|
||||||
// Find the first not-matching char position, if there is one
|
|
||||||
let differ_pos = line
|
|
||||||
.chars()
|
|
||||||
.zip(expanded.chars())
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_index, (a, b))| a != b)
|
|
||||||
.map(|(differ_pos, _)| differ_pos);
|
|
||||||
|
|
||||||
let pos = if let Some(differ_pos) = differ_pos {
|
|
||||||
if differ_pos < pos {
|
|
||||||
pos + (expanded.len() - line.len())
|
|
||||||
} else {
|
|
||||||
pos
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
pos
|
|
||||||
};
|
|
||||||
|
|
||||||
self.complete_internal(&expanded, pos, context)
|
|
||||||
.map_err(|e| ShellError::untagged_runtime_error(format!("{}", e)))
|
|
||||||
.map(requote)
|
|
||||||
.map(|(pos, completions)| {
|
|
||||||
(
|
|
||||||
pos,
|
|
||||||
completions
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|pair| completion::Suggestion {
|
.map(requote)
|
||||||
display: pair.display,
|
.collect();
|
||||||
replacement: pair.replacement,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &completion::Context<'_>) -> Option<String> {
|
(location.span.start(), suggestions)
|
||||||
self.hinter.hint(line, pos, &ctx.as_ref())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NuCompleter {
|
|
||||||
fn default() -> NuCompleter {
|
|
||||||
NuCompleter {
|
|
||||||
file_completer: FilenameCompleter::new(),
|
|
||||||
hinter: HistoryHinter {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_matching_arguments(
|
|
||||||
context: &context::Context,
|
|
||||||
lite_block: &nu_parser::LiteBlock,
|
|
||||||
line_chars: &[char],
|
|
||||||
line: &str,
|
|
||||||
replace_pos: usize,
|
|
||||||
pos: usize,
|
|
||||||
) -> Vec<rustyline::completion::Pair> {
|
|
||||||
let mut matching_arguments = vec![];
|
|
||||||
|
|
||||||
let mut line_copy = line.to_string();
|
|
||||||
let substring = line_chars[replace_pos..pos].iter().collect::<String>();
|
|
||||||
let replace_string = (replace_pos..pos).map(|_| " ").collect::<String>();
|
|
||||||
line_copy.replace_range(replace_pos..pos, &replace_string);
|
|
||||||
|
|
||||||
let result = nu_parser::classify_block(&lite_block, &context.registry);
|
|
||||||
|
|
||||||
for pipeline in &result.block.block {
|
|
||||||
for command in &pipeline.list {
|
|
||||||
if let nu_protocol::hir::ClassifiedCommand::Internal(
|
|
||||||
nu_protocol::hir::InternalCommand { args, .. },
|
|
||||||
) = command
|
|
||||||
{
|
|
||||||
if replace_pos >= args.span.start() && replace_pos <= args.span.end() {
|
|
||||||
if let Some(named) = &args.named {
|
|
||||||
for (name, _) in named.iter() {
|
|
||||||
let full_flag = format!("--{}", name);
|
|
||||||
|
|
||||||
if full_flag.starts_with(&substring) {
|
|
||||||
matching_arguments.push(rustyline::completion::Pair {
|
|
||||||
display: full_flag.clone(),
|
|
||||||
replacement: full_flag,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matching_arguments
|
|
||||||
}
|
|
||||||
|
|
||||||
// These is_executable/pathext implementations are copied from ichwh and modified
|
|
||||||
// to not be async
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn pathext() -> IchwhResult<Vec<String>> {
|
|
||||||
Ok(std::env::var_os("PATHEXT")
|
|
||||||
.ok_or(IchwhError::PathextNotDefined)?
|
|
||||||
.to_string_lossy()
|
|
||||||
.split(';')
|
|
||||||
// Cut off the leading '.' character
|
|
||||||
.map(|ext| ext[1..].to_string())
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn is_executable(file: &DirEntry) -> bool {
|
|
||||||
if let Ok(metadata) = file.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) = file.path().extension() {
|
|
||||||
if let Ok(exts) = pathext() {
|
|
||||||
exts.iter()
|
|
||||||
.any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))
|
|
||||||
} else {
|
} else {
|
||||||
false
|
(pos, Vec::new())
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
fn is_executable(_file: &DirEntry) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn is_executable(file: &DirEntry) -> bool {
|
|
||||||
let metadata = file.metadata();
|
|
||||||
|
|
||||||
if let Ok(metadata) = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_path_executables() -> Option<IndexSet<String>> {
|
|
||||||
let path_var = std::env::var_os("PATH")?;
|
|
||||||
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) = read_dir(path) {
|
|
||||||
while let Some(Ok(item)) = contents.next() {
|
|
||||||
if is_executable(&item) {
|
|
||||||
if let Ok(name) = item.file_name().into_string() {
|
|
||||||
executables.insert(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(executables)
|
fn requote(item: Suggestion) -> Suggestion {
|
||||||
}
|
|
||||||
|
|
||||||
fn get_replace_pos(line: &str, pos: usize) -> (usize, ReplacementLocation) {
|
|
||||||
let line_chars: Vec<_> = line[..pos].chars().collect();
|
|
||||||
let mut replace_pos = line_chars.len();
|
|
||||||
let mut parsed_pos = false;
|
|
||||||
let mut loc = ReplacementLocation::Other;
|
|
||||||
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
|
|
||||||
'outer: for pipeline in lite_block.block.iter() {
|
|
||||||
for command in pipeline.commands.iter() {
|
|
||||||
let name_span = command.name.span;
|
|
||||||
if name_span.start() <= pos && name_span.end() >= pos {
|
|
||||||
replace_pos = name_span.start();
|
|
||||||
parsed_pos = true;
|
|
||||||
loc = ReplacementLocation::Command;
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
|
|
||||||
for arg in command.args.iter() {
|
|
||||||
if arg.span.start() <= pos && arg.span.end() >= pos {
|
|
||||||
replace_pos = arg.span.start();
|
|
||||||
parsed_pos = true;
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !parsed_pos {
|
|
||||||
// If the command won't parse, naively detect the completion start point
|
|
||||||
while replace_pos > 0 {
|
|
||||||
if line_chars[replace_pos - 1] == ' ' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
replace_pos -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(replace_pos, loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requote(
|
|
||||||
items: (usize, Vec<rustyline::completion::Pair>),
|
|
||||||
) -> (usize, Vec<rustyline::completion::Pair>) {
|
|
||||||
let mut new_items = Vec::with_capacity(items.1.len());
|
|
||||||
|
|
||||||
for item in items.1 {
|
|
||||||
let unescaped = rustyline::completion::unescape(&item.replacement, Some('\\'));
|
let unescaped = rustyline::completion::unescape(&item.replacement, Some('\\'));
|
||||||
let maybe_quote = if unescaped != item.replacement {
|
if unescaped != item.replacement {
|
||||||
"\""
|
Suggestion {
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
new_items.push(rustyline::completion::Pair {
|
|
||||||
display: item.display,
|
display: item.display,
|
||||||
replacement: format!("{}{}{}", maybe_quote, unescaped, maybe_quote),
|
replacement: format!("\"{}\"", unescaped),
|
||||||
});
|
}
|
||||||
|
} else {
|
||||||
|
item
|
||||||
}
|
}
|
||||||
|
|
||||||
(items.0, new_items)
|
|
||||||
}
|
}
|
||||||
|
@ -282,8 +282,4 @@ impl completion::Completer for HelpShell {
|
|||||||
}
|
}
|
||||||
Ok((replace_pos, completions))
|
Ok((replace_pos, completions))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hint(&self, _line: &str, _pos: usize, _ctx: &completion::Context<'_>) -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
use crate::completion::{self, Completer};
|
use std::borrow::Cow::{self, Owned};
|
||||||
use crate::context::Context;
|
|
||||||
use crate::shell::palette::{DefaultPalette, Palette};
|
|
||||||
|
|
||||||
use ansi_term::{Color, Style};
|
use ansi_term::{Color, Style};
|
||||||
use nu_parser::SignatureRegistry;
|
use nu_parser::SignatureRegistry;
|
||||||
use nu_protocol::hir::FlatShape;
|
use nu_protocol::hir::FlatShape;
|
||||||
use nu_source::{Spanned, Tag, Tagged};
|
use nu_source::{Spanned, Tag, Tagged};
|
||||||
use rustyline::hint::Hinter;
|
|
||||||
use std::borrow::Cow::{self, Owned};
|
use crate::completion;
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::shell::completer::NuCompleter;
|
||||||
|
use crate::shell::palette::{DefaultPalette, Palette};
|
||||||
|
|
||||||
pub struct Helper {
|
pub struct Helper {
|
||||||
completer: Box<dyn Completer>,
|
completer: NuCompleter,
|
||||||
|
hinter: rustyline::hint::HistoryHinter,
|
||||||
context: Context,
|
context: Context,
|
||||||
pub colored_prompt: String,
|
pub colored_prompt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Helper {
|
impl Helper {
|
||||||
pub(crate) fn new(completer: Box<dyn Completer>, context: Context) -> Helper {
|
pub(crate) fn new(context: Context) -> Helper {
|
||||||
Helper {
|
Helper {
|
||||||
completer,
|
completer: NuCompleter {},
|
||||||
|
hinter: rustyline::hint::HistoryHinter {},
|
||||||
context,
|
context,
|
||||||
colored_prompt: String::new(),
|
colored_prompt: String::new(),
|
||||||
}
|
}
|
||||||
@ -45,16 +48,13 @@ impl rustyline::completion::Completer for Helper {
|
|||||||
ctx: &rustyline::Context<'_>,
|
ctx: &rustyline::Context<'_>,
|
||||||
) -> Result<(usize, Vec<Self::Candidate>), rustyline::error::ReadlineError> {
|
) -> Result<(usize, Vec<Self::Candidate>), rustyline::error::ReadlineError> {
|
||||||
let ctx = completion::Context::new(&self.context, ctx);
|
let ctx = completion::Context::new(&self.context, ctx);
|
||||||
self.completer
|
Ok(self.completer.complete(line, pos, &ctx))
|
||||||
.complete(line, pos, &ctx)
|
|
||||||
.map_err(|_| rustyline::error::ReadlineError::Eof)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hinter for Helper {
|
impl rustyline::hint::Hinter for Helper {
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
|
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
|
||||||
let ctx = completion::Context::new(&self.context, ctx);
|
self.hinter.hint(line, pos, &ctx)
|
||||||
self.completer.hint(line, pos, &ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,8 +307,4 @@ impl completion::Completer for ValueShell {
|
|||||||
}
|
}
|
||||||
Ok((replace_pos, completions))
|
Ok((replace_pos, completions))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hint(&self, _line: &str, _pos: usize, _context: &completion::Context<'_>) -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ use std::fmt::Debug;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ParseError<T: Debug> {
|
pub struct ParseError<T: Debug> {
|
||||||
/// An informative cause for this parse error
|
/// An informative cause for this parse error
|
||||||
pub(crate) cause: nu_errors::ParseError,
|
pub cause: nu_errors::ParseError,
|
||||||
|
|
||||||
/// What has been successfully parsed, if anything
|
/// What has been successfully parsed, if anything
|
||||||
pub(crate) partial: Option<T>,
|
pub partial: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ParseResult<T> = Result<T, ParseError<T>>;
|
pub type ParseResult<T> = Result<T, ParseError<T>>;
|
||||||
|
@ -133,11 +133,6 @@ fn bare(src: &mut Input, span_offset: usize) -> ParseResult<Spanned<String>> {
|
|||||||
// correct information from the non-lite parse.
|
// correct information from the non-lite parse.
|
||||||
bare.push(delimiter);
|
bare.push(delimiter);
|
||||||
|
|
||||||
let span = Span::new(
|
|
||||||
start_offset + span_offset,
|
|
||||||
start_offset + span_offset + bare.len(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(ParseError {
|
return Err(ParseError {
|
||||||
cause: nu_errors::ParseError::unexpected_eof(delimiter.to_string(), span),
|
cause: nu_errors::ParseError::unexpected_eof(delimiter.to_string(), span),
|
||||||
partial: Some(bare.spanned(span)),
|
partial: Some(bare.spanned(span)),
|
||||||
|
@ -1358,7 +1358,7 @@ fn classify_pipeline(
|
|||||||
}),
|
}),
|
||||||
positional: Some(args),
|
positional: Some(args),
|
||||||
named: None,
|
named: None,
|
||||||
span: Span::unknown(),
|
span: name_span,
|
||||||
external_redirection: if iter.peek().is_none() {
|
external_redirection: if iter.peek().is_none() {
|
||||||
ExternalRedirection::None
|
ExternalRedirection::None
|
||||||
} else {
|
} else {
|
||||||
@ -1448,7 +1448,7 @@ fn classify_pipeline(
|
|||||||
}),
|
}),
|
||||||
positional: Some(args),
|
positional: Some(args),
|
||||||
named: None,
|
named: None,
|
||||||
span: Span::unknown(),
|
span: name_span,
|
||||||
external_redirection: if iter.peek().is_none() {
|
external_redirection: if iter.peek().is_none() {
|
||||||
ExternalRedirection::None
|
ExternalRedirection::None
|
||||||
} else {
|
} else {
|
||||||
|
@ -526,10 +526,11 @@ impl Span {
|
|||||||
/// let span = Span::new(2, 8);
|
/// let span = Span::new(2, 8);
|
||||||
///
|
///
|
||||||
/// assert_eq!(span.contains(5), true);
|
/// assert_eq!(span.contains(5), true);
|
||||||
|
/// assert_eq!(span.contains(8), false);
|
||||||
/// assert_eq!(span.contains(100), false);
|
/// assert_eq!(span.contains(100), false);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn contains(&self, pos: usize) -> bool {
|
pub fn contains(&self, pos: usize) -> bool {
|
||||||
self.start <= pos && self.end >= pos
|
self.start <= pos && pos < self.end
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new Span by merging an earlier Span with the current Span.
|
/// Returns a new Span by merging an earlier Span with the current Span.
|
||||||
|
Loading…
Reference in New Issue
Block a user