Fixes autocomplete when using sudo (#8094)

# Description

This PR addresses issue #2047 in order to enable autocomplete
functionality when using sudo for executing commands. I'e done a couple
of auxiliary checks such as ignoring whitespace and the last pipe in
order to determine the last command.

# User-Facing Changes

The only user facing change should be the autocomplete working.

# Tests + Formatting

All tests and formatting pass.

# Screenshots

<img width="454" alt="image"
src="https://user-images.githubusercontent.com/4399118/219404037-6cce4358-68a9-42bb-a09b-2986b10fa6cc.png">

# Suggestions welcome

I still don't know the in's and out's if nushell very well, any
suggestions for improvements are welcome.
This commit is contained in:
alesito85 2023-02-25 00:05:36 +01:00 committed by GitHub
parent e93a8b1d32
commit 7c285750c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 175 additions and 3 deletions

View File

@ -199,6 +199,7 @@ impl Completer for CommandCompletion {
let commands = if matches!(self.flat_shape, nu_parser::FlatShape::External)
|| matches!(self.flat_shape, nu_parser::FlatShape::InternalCall)
|| ((span.end - span.start) == 0)
|| is_passthrough_command(working_set.delta.get_file_contents())
{
// we're in a gap or at a command
if working_set.get_span_contents(span).is_empty() && !self.force_completion_after_space
@ -226,3 +227,104 @@ impl Completer for CommandCompletion {
SortBy::LevenshteinDistance
}
}
pub fn find_non_whitespace_index(contents: &[u8], start: usize) -> usize {
match contents.get(start..) {
Some(contents) => {
contents
.iter()
.take_while(|x| x.is_ascii_whitespace())
.count()
+ start
}
None => start,
}
}
pub fn is_passthrough_command(working_set_file_contents: &[(Vec<u8>, usize, usize)]) -> bool {
for (contents, _, _) in working_set_file_contents {
let last_pipe_pos_rev = contents.iter().rev().position(|x| x == &b'|');
let last_pipe_pos = last_pipe_pos_rev.map(|x| contents.len() - x).unwrap_or(0);
let cur_pos = find_non_whitespace_index(contents, last_pipe_pos);
let result = match contents.get(cur_pos..) {
Some(contents) => contents.starts_with(b"sudo "),
None => false,
};
if result {
return true;
}
}
false
}
#[cfg(test)]
mod command_completions_tests {
use super::*;
#[test]
fn test_find_non_whitespace_index() {
let commands = vec![
(" hello", 4),
("sudo ", 0),
(" sudo ", 2),
(" sudo ", 2),
(" hello ", 1),
(" hello ", 3),
(" hello | sudo ", 4),
(" sudo|sudo", 5),
("sudo | sudo ", 0),
(" hello sud", 1),
];
for (idx, ele) in commands.iter().enumerate() {
let index = find_non_whitespace_index(&Vec::from(ele.0.as_bytes()), 0);
assert_eq!(index, ele.1, "Failed on index {}", idx);
}
}
#[test]
fn test_is_last_command_passthrough() {
let commands = vec![
(" hello", false),
(" sudo ", true),
("sudo ", true),
(" hello", false),
(" sudo", false),
(" sudo ", true),
(" sudo ", true),
(" sudo ", true),
(" hello ", false),
(" hello | sudo ", true),
(" sudo|sudo", false),
("sudo | sudo ", true),
(" hello sud", false),
(" sudo | sud ", false),
(" sudo|sudo ", true),
(" sudo | sudo ls | sudo ", true),
];
for (idx, ele) in commands.iter().enumerate() {
let input = ele.0.as_bytes();
let mut engine_state = EngineState::new();
engine_state.add_file("test.nu".into(), vec![]);
let delta = {
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_file("child.nu".into(), input);
working_set.render()
};
if let Err(err) = engine_state.merge_delta(delta) {
assert!(false, "Merge delta has failed: {}", err);
}
let is_passthrough_command = is_passthrough_command(engine_state.get_file_contents());
assert_eq!(
is_passthrough_command, ele.1,
"index for '{}': {}",
ele.0, idx
);
}
}
}

View File

@ -135,6 +135,10 @@ impl NuCompleter {
let mut spans: Vec<String> = vec![];
for (flat_idx, flat) in flattened.iter().enumerate() {
let is_passthrough_command = spans
.first()
.filter(|content| *content == &String::from("sudo"))
.is_some();
// 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(&current_span);
@ -217,8 +221,9 @@ impl NuCompleter {
}
// specially check if it is currently empty - always complete commands
if flat_idx == 0
&& working_set.get_span_contents(new_span).is_empty()
if (is_passthrough_command && flat_idx == 1)
|| (flat_idx == 0
&& working_set.get_span_contents(new_span).is_empty())
{
let mut completer = CommandCompletion::new(
self.engine_state.clone(),
@ -239,7 +244,7 @@ impl NuCompleter {
}
// Completions that depends on the previous expression (e.g: use, source-env)
if flat_idx > 0 {
if (is_passthrough_command && flat_idx > 1) || flat_idx > 0 {
if let Some(previous_expr) = flattened.get(flat_idx - 1) {
// Read the content for the previous expression
let prev_expr_str =
@ -576,3 +581,60 @@ pub fn map_value_completions<'a>(
})
.collect()
}
#[cfg(test)]
mod completer_tests {
use super::*;
#[test]
fn test_completion_helper() {
let mut engine_state = nu_command::create_default_context();
// Custom additions
let delta = {
let working_set = nu_protocol::engine::StateWorkingSet::new(&engine_state);
working_set.render()
};
if let Err(err) = engine_state.merge_delta(delta) {
assert!(false, "Error merging delta: {:?}", err);
}
let mut completer = NuCompleter::new(engine_state.into(), Stack::new());
let dataset = vec![
("sudo", false, "", Vec::new()),
("sudo l", true, "l", vec!["ls", "let", "lines", "loop"]),
(" sudo", false, "", Vec::new()),
(" sudo le", true, "le", vec!["let", "length"]),
(
"ls | c",
true,
"c",
vec!["cd", "config", "const", "cp", "cal"],
),
("ls | sudo m", true, "m", vec!["mv", "mut", "move"]),
];
for (line, has_result, begins_with, expected_values) in dataset {
let result = completer.completion_helper(line, line.len());
// Test whether the result is empty or not
assert_eq!(result.len() > 0, has_result, "line: {}", line);
// Test whether the result begins with the expected value
result
.iter()
.for_each(|x| assert!(x.value.starts_with(begins_with)));
// Test whether the result contains all the expected values
assert_eq!(
result
.iter()
.map(|x| expected_values.contains(&x.value.as_str()))
.filter(|x| *x == true)
.count(),
expected_values.len(),
"line: {}",
line
);
}
}
}

View File

@ -1007,6 +1007,10 @@ impl EngineState {
.map(|d| d.as_string().unwrap_or_default())
.unwrap_or_default()
}
pub fn get_file_contents(&self) -> &Vec<(Vec<u8>, usize, usize)> {
&self.file_contents
}
}
/// A temporary extension to the global state. This handles bridging between the global state and the
@ -1191,6 +1195,10 @@ impl StateDelta {
pub fn exit_scope(&mut self) {
self.scope.pop();
}
pub fn get_file_contents(&self) -> &Vec<(Vec<u8>, usize, usize)> {
&self.file_contents
}
}
impl<'a> StateWorkingSet<'a> {