forked from extern/nushell
feat: external completions for commands/flags (#6295)
* wip * wip * cleanup * error message * cleanup * cleanup * fix clippy * add test * fix span * cleanup * cleanup * cleanup * fixed completion * push char * wip * small fixes * fix remove last span * fmt * cleanup * fixes + more tests * fix test * only complete for commands * also complete flags * change decl_id to block_id * use nu completion first * fix test * ignore test * update config section
This commit is contained in:
parent
772ad896c8
commit
646aace05b
@ -2,10 +2,11 @@ use crate::completions::{
|
|||||||
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
|
||||||
DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion,
|
DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion,
|
||||||
};
|
};
|
||||||
|
use nu_engine::eval_block;
|
||||||
use nu_parser::{flatten_expression, parse, FlatShape};
|
use nu_parser::{flatten_expression, parse, FlatShape};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
Span,
|
BlockId, PipelineData, Span, Value,
|
||||||
};
|
};
|
||||||
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||||
use std::str;
|
use std::str;
|
||||||
@ -56,6 +57,67 @@ impl NuCompleter {
|
|||||||
suggestions
|
suggestions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn external_completion(
|
||||||
|
&self,
|
||||||
|
block_id: BlockId,
|
||||||
|
spans: Vec<String>,
|
||||||
|
offset: usize,
|
||||||
|
span: Span,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
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
|
||||||
|
.into_iter()
|
||||||
|
.map(|it| Value::String {
|
||||||
|
val: it,
|
||||||
|
span: Span::unknown(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = eval_block(
|
||||||
|
&self.engine_state,
|
||||||
|
&mut callee_stack,
|
||||||
|
block,
|
||||||
|
PipelineData::new(span),
|
||||||
|
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 {
|
||||||
|
start: span.start,
|
||||||
|
end: span.end,
|
||||||
|
},
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => println!("failed to eval completer block: {}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||||
let offset = working_set.next_span_start();
|
let offset = working_set.next_span_start();
|
||||||
@ -63,14 +125,32 @@ impl NuCompleter {
|
|||||||
let initial_line = line.to_string();
|
let initial_line = line.to_string();
|
||||||
new_line.push(b'a');
|
new_line.push(b'a');
|
||||||
let pos = offset + pos;
|
let pos = offset + pos;
|
||||||
|
let config = self.engine_state.get_config();
|
||||||
|
|
||||||
let (output, _err) = parse(&mut working_set, Some("completer"), &new_line, false, &[]);
|
let (output, _err) = parse(&mut working_set, Some("completer"), &new_line, false, &[]);
|
||||||
|
|
||||||
for pipeline in output.pipelines.into_iter() {
|
for pipeline in output.pipelines.into_iter() {
|
||||||
for expr in pipeline.expressions {
|
for expr in pipeline.expressions {
|
||||||
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
let flattened: Vec<_> = flatten_expression(&working_set, &expr);
|
||||||
let span_offset: usize = alias_offset.iter().sum();
|
let span_offset: usize = alias_offset.iter().sum();
|
||||||
|
let mut spans: Vec<String> = vec![];
|
||||||
|
|
||||||
for (flat_idx, flat) in flattened.iter().enumerate() {
|
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 {
|
if pos + span_offset >= flat.0.start && pos + span_offset < flat.0.end {
|
||||||
// Context variables
|
// Context variables
|
||||||
let most_left_var =
|
let most_left_var =
|
||||||
@ -113,16 +193,26 @@ impl NuCompleter {
|
|||||||
|
|
||||||
// Flags completion
|
// Flags completion
|
||||||
if prefix.starts_with(b"-") {
|
if prefix.starts_with(b"-") {
|
||||||
let mut completer = FlagCompletion::new(expr);
|
// Try to complete flag internally
|
||||||
|
let mut completer = FlagCompletion::new(expr.clone());
|
||||||
return self.process_completion(
|
let result = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix,
|
prefix.clone(),
|
||||||
new_span,
|
new_span,
|
||||||
offset,
|
offset,
|
||||||
pos,
|
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 {
|
||||||
|
return self.external_completion(block_id, spans, offset, new_span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completions that depends on the previous expression (e.g: use, source)
|
// Completions that depends on the previous expression (e.g: use, source)
|
||||||
@ -214,7 +304,7 @@ impl NuCompleter {
|
|||||||
flat_shape.clone(),
|
flat_shape.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let out: Vec<_> = self.process_completion(
|
let mut out: Vec<_> = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix.clone(),
|
prefix.clone(),
|
||||||
@ -223,11 +313,13 @@ impl NuCompleter {
|
|||||||
pos,
|
pos,
|
||||||
);
|
);
|
||||||
|
|
||||||
if out.is_empty() {
|
if !out.is_empty() {
|
||||||
let mut completer =
|
return out;
|
||||||
FileCompletion::new(self.engine_state.clone());
|
}
|
||||||
|
|
||||||
return self.process_completion(
|
// Check for file completion
|
||||||
|
let mut completer = FileCompletion::new(self.engine_state.clone());
|
||||||
|
out = self.process_completion(
|
||||||
&mut completer,
|
&mut completer,
|
||||||
&working_set,
|
&working_set,
|
||||||
prefix,
|
prefix,
|
||||||
@ -235,9 +327,16 @@ impl NuCompleter {
|
|||||||
offset,
|
offset,
|
||||||
pos,
|
pos,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if !out.is_empty() {
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
// Try to complete using an exnternal compelter (if set)
|
||||||
|
if let Some(block_id) = config.external_completer {
|
||||||
|
return self
|
||||||
|
.external_completion(block_id, spans, offset, new_span);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -383,3 +482,65 @@ fn most_left_variable(
|
|||||||
|
|
||||||
Some((var, sublevels))
|
Some((var, sublevels))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_value_completions<'a>(
|
||||||
|
list: impl Iterator<Item = &'a Value>,
|
||||||
|
span: Span,
|
||||||
|
offset: usize,
|
||||||
|
) -> Vec<Suggestion> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
@ -8,6 +8,8 @@ use nu_protocol::{
|
|||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::completer::map_value_completions;
|
||||||
|
|
||||||
pub struct CustomCompletion {
|
pub struct CustomCompletion {
|
||||||
engine_state: Arc<EngineState>,
|
engine_state: Arc<EngineState>,
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
@ -26,69 +28,6 @@ impl CustomCompletion {
|
|||||||
sort_by: SortBy::None,
|
sort_by: SortBy::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_completions<'a>(
|
|
||||||
&self,
|
|
||||||
list: impl Iterator<Item = &'a Value>,
|
|
||||||
span: Span,
|
|
||||||
offset: usize,
|
|
||||||
) -> Vec<Suggestion> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for CustomCompletion {
|
impl Completer for CustomCompletion {
|
||||||
@ -144,7 +83,7 @@ impl Completer for CustomCompletion {
|
|||||||
.and_then(|val| {
|
.and_then(|val| {
|
||||||
val.as_list()
|
val.as_list()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|it| self.map_completions(it.iter(), span, offset))
|
.map(|it| map_value_completions(it.iter(), span, offset))
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let options = value.get_data_by_key("options");
|
let options = value.get_data_by_key("options");
|
||||||
@ -189,7 +128,7 @@ impl Completer for CustomCompletion {
|
|||||||
|
|
||||||
completions
|
completions
|
||||||
}
|
}
|
||||||
Value::List { vals, .. } => self.map_completions(vals.iter(), span, offset),
|
Value::List { vals, .. } => map_value_completions(vals.iter(), span, offset),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
crates/nu-cli/tests/external_completions.rs
Normal file
73
crates/nu-cli/tests/external_completions.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
pub mod support;
|
||||||
|
|
||||||
|
use nu_cli::NuCompleter;
|
||||||
|
use nu_parser::parse;
|
||||||
|
use nu_protocol::engine::StateWorkingSet;
|
||||||
|
use reedline::{Completer, Suggestion};
|
||||||
|
use support::new_engine;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn external_completer_trailing_space() {
|
||||||
|
// https://github.com/nushell/nushell/issues/6378
|
||||||
|
let block = "let external_completer = {|spans| $spans}";
|
||||||
|
let input = "gh alias ".to_string();
|
||||||
|
|
||||||
|
let suggestions = run_completion(&block, &input);
|
||||||
|
assert_eq!(3, suggestions.len());
|
||||||
|
assert_eq!("gh", suggestions.get(0).unwrap().value);
|
||||||
|
assert_eq!("alias", suggestions.get(1).unwrap().value);
|
||||||
|
assert_eq!("", suggestions.get(2).unwrap().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_completer_no_trailing_space() {
|
||||||
|
let block = "let external_completer = {|spans| $spans}";
|
||||||
|
let input = "gh alias".to_string();
|
||||||
|
|
||||||
|
let suggestions = run_completion(&block, &input);
|
||||||
|
assert_eq!(2, suggestions.len());
|
||||||
|
assert_eq!("gh", suggestions.get(0).unwrap().value);
|
||||||
|
assert_eq!("alias", suggestions.get(1).unwrap().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_completer_pass_flags() {
|
||||||
|
let block = "let external_completer = {|spans| $spans}";
|
||||||
|
let input = "gh api --".to_string();
|
||||||
|
|
||||||
|
let suggestions = run_completion(&block, &input);
|
||||||
|
assert_eq!(3, suggestions.len());
|
||||||
|
assert_eq!("gh", suggestions.get(0).unwrap().value);
|
||||||
|
assert_eq!("api", suggestions.get(1).unwrap().value);
|
||||||
|
assert_eq!("--", suggestions.get(2).unwrap().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_completion(block: &str, input: &str) -> Vec<Suggestion> {
|
||||||
|
// Create a new engine
|
||||||
|
let (dir, _, mut engine_state, mut stack) = new_engine();
|
||||||
|
let (_, delta) = {
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
let (block, err) = parse(&mut working_set, None, block.as_bytes(), false, &[]);
|
||||||
|
assert!(err.is_none());
|
||||||
|
|
||||||
|
(block, working_set.render())
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(engine_state.merge_delta(delta).is_ok());
|
||||||
|
|
||||||
|
// Merge environment into the permanent state
|
||||||
|
assert!(engine_state.merge_env(&mut stack, &dir).is_ok());
|
||||||
|
|
||||||
|
let latest_block_id = engine_state.num_blocks() - 1;
|
||||||
|
|
||||||
|
// Change config adding the external completer
|
||||||
|
let mut config = engine_state.get_config().clone();
|
||||||
|
config.external_completer = Some(latest_block_id);
|
||||||
|
engine_state.set_config(&config);
|
||||||
|
|
||||||
|
// Instatiate a new completer
|
||||||
|
let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack);
|
||||||
|
|
||||||
|
completer.complete(&input, input.len())
|
||||||
|
}
|
@ -52,6 +52,7 @@ impl Default for Hooks {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
pub external_completer: Option<usize>,
|
||||||
pub filesize_metric: bool,
|
pub filesize_metric: bool,
|
||||||
pub table_mode: String,
|
pub table_mode: String,
|
||||||
pub use_ls_colors: bool,
|
pub use_ls_colors: bool,
|
||||||
@ -90,6 +91,7 @@ impl Default for Config {
|
|||||||
Config {
|
Config {
|
||||||
filesize_metric: false,
|
filesize_metric: false,
|
||||||
table_mode: "rounded".into(),
|
table_mode: "rounded".into(),
|
||||||
|
external_completer: None,
|
||||||
use_ls_colors: true,
|
use_ls_colors: true,
|
||||||
color_config: HashMap::new(),
|
color_config: HashMap::new(),
|
||||||
use_grid_icons: false,
|
use_grid_icons: false,
|
||||||
@ -183,6 +185,11 @@ impl Value {
|
|||||||
eprintln!("$config.filesize_metric is not a bool")
|
eprintln!("$config.filesize_metric is not a bool")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"external_completer" => {
|
||||||
|
if let Ok(v) = value.as_block() {
|
||||||
|
config.external_completer = Some(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
"table_mode" => {
|
"table_mode" => {
|
||||||
if let Ok(v) = value.as_string() {
|
if let Ok(v) = value.as_string() {
|
||||||
config.table_mode = v;
|
config.table_mode = v;
|
||||||
|
@ -231,8 +231,15 @@ let light_theme = {
|
|||||||
shape_nothing: light_cyan
|
shape_nothing: light_cyan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# External completer example
|
||||||
|
# let carapace_completer = {|spans|
|
||||||
|
# carapace $spans.0 nushell $spans | from json
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
# The default config record. This is where much of your global configuration is setup.
|
# The default config record. This is where much of your global configuration is setup.
|
||||||
let-env config = {
|
let-env config = {
|
||||||
|
external_completer: $nothing # check 'carapace_completer' above to as example
|
||||||
filesize_metric: false
|
filesize_metric: false
|
||||||
table_mode: rounded # basic, compact, compact_double, light, thin, with_love, rounded, reinforced, heavy, none, other
|
table_mode: rounded # basic, compact, compact_double, light, thin, with_love, rounded, reinforced, heavy, none, other
|
||||||
use_ls_colors: true
|
use_ls_colors: true
|
||||||
|
Loading…
Reference in New Issue
Block a user