use nu_engine::eval_block; use nu_parser::{flatten_expression, parse, trim_quotes, FlatShape}; use nu_protocol::{ ast::Expr, engine::{EngineState, Stack, StateWorkingSet}, PipelineData, Span, Value, CONFIG_VARIABLE_ID, }; use reedline::Completer; const SEP: char = std::path::MAIN_SEPARATOR; #[derive(Clone)] pub struct NuCompleter { engine_state: EngineState, config: Option<Value>, } impl NuCompleter { pub fn new(engine_state: EngineState, config: Option<Value>) -> Self { Self { engine_state, config, } } fn external_command_completion(&self, prefix: &str) -> Vec<String> { let mut executables = vec![]; let paths; paths = self.engine_state.env_vars.get("PATH"); if let Some(paths) = paths { if let Ok(paths) = paths.as_list() { for path in paths { let path = path.as_string().unwrap_or_default(); if let Ok(mut contents) = std::fs::read_dir(path) { while let Some(Ok(item)) = contents.next() { if !executables.contains( &item .path() .file_name() .map(|x| x.to_string_lossy().to_string()) .unwrap_or_default(), ) && matches!( item.path() .file_name() .map(|x| x.to_string_lossy().starts_with(prefix)), Some(true) ) && is_executable::is_executable(&item.path()) { if let Ok(name) = item.file_name().into_string() { executables.push(name); } } } } } } } executables } fn complete_variables( &self, working_set: &StateWorkingSet, prefix: &[u8], span: Span, offset: usize, ) -> Vec<(reedline::Span, String)> { let mut output = vec![]; let builtins = [ "$nu", "$scope", "$in", "$config", "$env", "$true", "$false", "$nothing", ]; for builtin in builtins { if builtin.as_bytes().starts_with(prefix) { output.push(( reedline::Span { start: span.start - offset, end: span.end - offset, }, builtin.to_string(), )); } } for scope in &working_set.delta.scope { for v in &scope.vars { if v.0.starts_with(prefix) { output.push(( reedline::Span { start: span.start - offset, end: span.end - offset, }, String::from_utf8_lossy(v.0).to_string(), )); } } } for scope in &self.engine_state.scope { for v in &scope.vars { if v.0.starts_with(prefix) { output.push(( reedline::Span { start: span.start - offset, end: span.end - offset, }, String::from_utf8_lossy(v.0).to_string(), )); } } } output.dedup(); output } fn complete_commands( &self, working_set: &StateWorkingSet, span: Span, offset: usize, find_externals: bool, ) -> Vec<(reedline::Span, String)> { let prefix = working_set.get_span_contents(span); let mut results = working_set .find_commands_by_prefix(prefix) .into_iter() .map(move |x| { ( reedline::Span { start: span.start - offset, end: span.end - offset, }, String::from_utf8_lossy(&x).to_string(), ) }) .collect::<Vec<_>>(); let prefix = working_set.get_span_contents(span); let prefix = String::from_utf8_lossy(prefix).to_string(); if find_externals { let results_external = self.external_command_completion(&prefix) .into_iter() .map(move |x| { ( reedline::Span { start: span.start - offset, end: span.end - offset, }, x, ) }); for external in results_external { if results.contains(&external) { results.push((external.0, format!("^{}", external.1))) } else { results.push(external) } } results } else { results } } fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { let mut working_set = StateWorkingSet::new(&self.engine_state); let offset = working_set.next_span_start(); let 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 { 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"$") { return self.complete_variables( &working_set, &prefix, new_span, offset, ); } 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 { 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(( reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, String::from_utf8_lossy(&named).to_string(), )); } } 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(( reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, String::from_utf8_lossy(&named).to_string(), )); } } return output; } } match &flat.1 { FlatShape::Custom(custom_completion) => { //let prefix = working_set.get_span_contents(flat.0).to_vec(); let (block, ..) = parse( &mut working_set, None, custom_completion.as_bytes(), false, ); let mut stack = Stack::new(); // 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_block( &self.engine_state, &mut stack, &block, PipelineData::new(new_span), ); let v: Vec<_> = match result { Ok(pd) => pd .into_iter() .filter_map(move |x| { let s = x.as_string(); match s { Ok(s) => Some(( reedline::Span { start: new_span.start - offset, end: new_span.end - offset, }, s, )), Err(_) => None, } }) .filter(|x| x.1.as_bytes().starts_with(&prefix)) .collect(), _ => vec![], }; return v; } 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(); return file_path_completion(new_span, &prefix, &cwd) .into_iter() .map(move |x| { ( reedline::Span { start: x.0.start - offset, end: x.0.end - offset, }, x.1, ) }) .collect(); } 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 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| { ( reedline::Span { start: x.0.start - offset, end: x.0.end - offset, }, x.1, ) }) .chain(subcommands.into_iter()) .chain(commands.into_iter()) .collect::<Vec<_>>(); //output.dedup_by(|a, b| a.1 == b.1); return output; } } } } } } vec![] } } impl Completer for NuCompleter { fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { let mut output = self.completion_helper(line, pos); output.sort_by(|a, b| a.1.cmp(&b.1)); output } } fn file_path_completion( span: nu_protocol::Span, partial: &str, cwd: &str, ) -> Vec<(nu_protocol::Span, String)> { use std::path::{is_separator, Path}; let partial = 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() { result .filter_map(|entry| { entry.ok().and_then(|entry| { let mut file_name = entry.file_name().to_string_lossy().into_owned(); if matches(partial, &file_name) { let mut path = format!("{}{}", base_dir_name, file_name); if entry.path().is_dir() { path.push(SEP); file_name.push(SEP); } if path.contains(' ') { path = format!("\"{}\"", path); } Some((span, path)) } else { None } }) }) .collect() } else { Vec::new() } } fn matches(partial: &str, from: &str) -> bool { from.to_ascii_lowercase() .starts_with(&partial.to_ascii_lowercase()) }