use crate::completions::{ CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion, }; use nu_engine::eval_block; use nu_parser::{flatten_expression, parse, FlatShape}; use nu_protocol::{ ast::PipelineElement, engine::{EngineState, Stack, StateWorkingSet}, BlockId, PipelineData, Span, Value, }; use reedline::{Completer as ReedlineCompleter, Suggestion}; use std::str; use std::sync::Arc; #[derive(Clone)] pub struct NuCompleter { engine_state: Arc, stack: Stack, } impl NuCompleter { pub fn new(engine_state: Arc, stack: Stack) -> Self { Self { engine_state, stack, } } // Process the completion for a given completer fn process_completion( &self, completer: &mut T, working_set: &StateWorkingSet, prefix: Vec, new_span: Span, offset: usize, pos: usize, ) -> Vec { let config = self.engine_state.get_config(); let mut options = CompletionOptions { case_sensitive: config.case_sensitive_completions, ..Default::default() }; if config.completion_algorithm == "fuzzy" { options.match_algorithm = MatchAlgorithm::Fuzzy; } // Fetch let mut suggestions = completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options); // Sort suggestions = completer.sort(suggestions, prefix); suggestions } fn external_completion( &self, block_id: BlockId, spans: &[String], offset: usize, span: Span, ) -> Option> { let stack = self.stack.clone(); let block = self.engine_state.get_block(block_id); let mut callee_stack = stack.gather_captures(&block.captures); // Line if let Some(pos_arg) = block.signature.required_positional.get(0) { if let Some(var_id) = pos_arg.var_id { callee_stack.add_var( var_id, Value::List { vals: spans .iter() .map(|it| Value::string(it, Span::unknown())) .collect(), span: Span::unknown(), }, ); } } let result = eval_block( &self.engine_state, &mut callee_stack, block, PipelineData::empty(), true, true, ); match result { Ok(pd) => { let value = pd.into_value(span); if let Value::List { vals, span: _ } = value { let result = map_value_completions(vals.iter(), Span::new(span.start, span.end), offset); return Some(result); } } Err(err) => println!("failed to eval completer block: {}", err), } None } fn completion_helper(&mut self, line: &str, pos: usize) -> Vec { let mut working_set = StateWorkingSet::new(&self.engine_state); let offset = working_set.next_span_start(); let (mut new_line, alias_offset) = try_find_alias(line.as_bytes(), &working_set); let initial_line = line.to_string(); let alias_total_offset: usize = alias_offset.iter().sum(); new_line.insert(alias_total_offset + pos, b'a'); let pos = offset + pos; let config = self.engine_state.get_config(); let (output, _err) = parse(&mut working_set, Some("completer"), &new_line, false, &[]); for pipeline in output.pipelines.into_iter() { for pipeline_element in pipeline.elements { match pipeline_element { PipelineElement::Expression(_, expr) | PipelineElement::Redirection(_, _, expr) | PipelineElement::Or(_, expr) => { let flattened: Vec<_> = flatten_expression(&working_set, &expr); let span_offset: usize = alias_offset.iter().sum(); let mut spans: Vec = vec![]; for (flat_idx, flat) in flattened.iter().enumerate() { // Read the current spam to string let current_span = working_set.get_span_contents(flat.0).to_vec(); let current_span_str = String::from_utf8_lossy(¤t_span); // Skip the last 'a' as span item if flat_idx == flattened.len() - 1 { let mut chars = current_span_str.chars(); chars.next_back(); let current_span_str = chars.as_str().to_owned(); spans.push(current_span_str.to_string()); } else { spans.push(current_span_str.to_string()); } // Complete based on the last span if pos + span_offset >= flat.0.start && pos + span_offset < flat.0.end { // Context variables let most_left_var = most_left_variable(flat_idx, &working_set, flattened.clone()); // Create a new span let new_span = if flat_idx == 0 { Span::new(flat.0.start, flat.0.end - 1 - span_offset) } else { Span::new( flat.0.start - span_offset, flat.0.end - 1 - span_offset, ) }; // Parses the prefix. Completion should look up to the cursor position, not after. let mut prefix = working_set.get_span_contents(flat.0).to_vec(); let index = pos - (flat.0.start - span_offset); prefix.drain(index..); // Variables completion if prefix.starts_with(b"$") || most_left_var.is_some() { let mut completer = VariableCompletion::new( self.engine_state.clone(), self.stack.clone(), most_left_var.unwrap_or((vec![], vec![])), ); return self.process_completion( &mut completer, &working_set, prefix, new_span, offset, pos, ); } // Flags completion if prefix.starts_with(b"-") { // Try to complete flag internally let mut completer = FlagCompletion::new(expr.clone()); let result = self.process_completion( &mut completer, &working_set, prefix.clone(), new_span, offset, pos, ); if !result.is_empty() { return result; } // We got no results for internal completion // now we can check if external completer is set and use it if let Some(block_id) = config.external_completer { if let Some(external_result) = self .external_completion(block_id, &spans, offset, new_span) { return external_result; } } } // specially check if it is currently empty - always complete commands if flat_idx == 0 && working_set.get_span_contents(new_span).is_empty() { let mut completer = CommandCompletion::new( self.engine_state.clone(), &working_set, flattened.clone(), // flat_idx, FlatShape::String, true, ); return self.process_completion( &mut completer, &working_set, prefix, new_span, offset, pos, ); } // Completions that depends on the previous expression (e.g: use, source-env) if flat_idx > 0 { if let Some(previous_expr) = flattened.get(flat_idx - 1) { // Read the content for the previous expression let prev_expr_str = working_set.get_span_contents(previous_expr.0).to_vec(); // Completion for .nu files if prev_expr_str == b"use" || prev_expr_str == b"source-env" { let mut completer = DotNuCompletion::new(self.engine_state.clone()); return self.process_completion( &mut completer, &working_set, prefix, new_span, offset, pos, ); } else if prev_expr_str == b"ls" { let mut completer = FileCompletion::new(self.engine_state.clone()); 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(), *decl_id, initial_line, ); return self.process_completion( &mut completer, &working_set, prefix, new_span, offset, pos, ); } FlatShape::Directory => { let mut completer = DirectoryCompletion::new(self.engine_state.clone()); 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(), false, ); let mut out: Vec<_> = self.process_completion( &mut completer, &working_set, prefix.clone(), new_span, offset, pos, ); if !out.is_empty() { return out; } // Try to complete using an external completer (if set) if let Some(block_id) = config.external_completer { if let Some(external_result) = self.external_completion( block_id, &spans, offset, new_span, ) { return external_result; } } // Check for file completion let mut completer = FileCompletion::new(self.engine_state.clone()); out = self.process_completion( &mut completer, &working_set, prefix, new_span, offset, pos, ); if !out.is_empty() { return out; } } }; } } } } } } vec![] } } impl ReedlineCompleter for NuCompleter { fn complete(&mut self, line: &str, pos: usize) -> Vec { self.completion_helper(line, pos) } } type MatchedAlias = Vec<(Vec, Vec)>; // Handler the completion when giving lines contains at least one alias. (e.g: `g checkout`) // that `g` is an alias of `git` fn try_find_alias(line: &[u8], working_set: &StateWorkingSet) -> (Vec, Vec) { // An vector represents the offsets of alias // e.g: the offset is 2 for the alias `g` of `git` let mut alias_offset = vec![]; let mut output = vec![]; if let Some(matched_alias) = search_alias(line, working_set) { let mut lens = matched_alias.len(); for (input_vec, line_vec) in matched_alias { alias_offset.push(line_vec.len() - input_vec.len()); output.extend(line_vec); if lens > 1 { output.push(b' '); lens -= 1; } } if !line.is_empty() { let last = line.last().expect("input is empty"); if last == &b' ' { output.push(b' '); } } } else { output = line.to_vec(); } (output, alias_offset) } fn search_alias(input: &[u8], working_set: &StateWorkingSet) -> Option { let mut vec_names = vec![]; let mut vec_alias = vec![]; let mut pos = 0; let mut is_alias = false; for (index, character) in input.iter().enumerate() { if *character == b' ' { let range = &input[pos..index]; vec_names.push(range.to_owned()); pos = index + 1; } } // Push the rest to names vector. if pos < input.len() { vec_names.push(input[pos..].to_owned()); } for name in &vec_names { if let Some(alias_id) = working_set.find_alias(&name[..]) { let alias_span = working_set.get_alias(alias_id); let mut span_vec = vec![]; is_alias = true; for alias in alias_span { let name = working_set.get_span_contents(*alias); if !name.is_empty() { span_vec.push(name); } } // Join span of vector together for complex alias, e.g: `f` is an alias for `git remote -v` let full_aliases = span_vec.join(&[b' '][..]); vec_alias.push(full_aliases); } else { vec_alias.push(name.to_owned()); } } if is_alias { // Zip names and alias vectors, the original inputs and its aliases mapping. // e.g:(['g'], ['g','i','t']) let output = vec_names.into_iter().zip(vec_alias).collect(); Some(output) } else { None } } // reads the most left variable returning it's name (e.g: $myvar) // and the depth (a.b.c) fn most_left_variable( idx: usize, working_set: &StateWorkingSet<'_>, flattened: Vec<(Span, FlatShape)>, ) -> Option<(Vec, Vec>)> { // Reverse items to read the list backwards and truncate // because the only items that matters are the ones before the current index let mut rev = flattened; rev.truncate(idx); rev = rev.into_iter().rev().collect(); // Store the variables and sub levels found and reverse to correct order let mut variables_found: Vec> = vec![]; let mut found_var = false; for item in rev.clone() { let result = working_set.get_span_contents(item.0).to_vec(); match item.1 { FlatShape::Variable => { variables_found.push(result); found_var = true; break; } FlatShape::String => { variables_found.push(result); } _ => { break; } } } // If most left var was not found if !found_var { return None; } // Reverse the order back variables_found = variables_found.into_iter().rev().collect(); // Extract the variable and the sublevels let var = variables_found.first().unwrap_or(&vec![]).to_vec(); let sublevels: Vec> = variables_found.into_iter().skip(1).collect(); Some((var, sublevels)) } pub fn map_value_completions<'a>( list: impl Iterator, span: Span, offset: usize, ) -> Vec { list.filter_map(move |x| { // Match for string values if let Ok(s) = x.as_string() { return Some(Suggestion { value: s, description: None, extra: None, span: reedline::Span { start: span.start - offset, end: span.end - offset, }, append_whitespace: false, }); } // Match for record values if let Ok((cols, vals)) = x.as_record() { let mut suggestion = Suggestion { value: String::from(""), // Initialize with empty string description: None, extra: None, span: reedline::Span { start: span.start - offset, end: span.end - offset, }, append_whitespace: false, }; // Iterate the cols looking for `value` and `description` cols.iter().zip(vals).for_each(|it| { // Match `value` column if it.0 == "value" { // Convert the value to string if let Ok(val_str) = it.1.as_string() { // Update the suggestion value suggestion.value = val_str; } } // Match `description` column if it.0 == "description" { // Convert the value to string if let Ok(desc_str) = it.1.as_string() { // Update the suggestion value suggestion.description = Some(desc_str); } } }); return Some(suggestion); } None }) .collect() }