mirror of
https://github.com/nushell/nushell.git
synced 2024-12-05 23:00:50 +01:00
9e738193f3
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR fixes the problem pointed out in https://github.com/nushell/nushell/issues/13204, where the Fish-like completions aren't sorted properly (this PR doesn't close that issue because the author there wants more than just fixed sort order). The cause is all of the file/directory completions being fetched first and then sorted all together while being treated as strings. Instead, this PR sorts completions within each individual directory, avoiding treating `/` as part of the path. To do this, I removed the `sort` method from the completer trait (as well as `get_sort_by`) and made all completers sort within the `fetch` method itself. A generic `sort_completions` helper has been added to sort lists of completions, and a more specific `sort_suggestions` helper has been added to sort `Vec<Suggestion>`s. As for the actual change that fixes the sort order for file/directory completions, the `complete_rec` helper now sorts the children of each directory before visiting their children. The file and directory completers don't bother sorting at the end (except to move hidden files down). To reviewers: don't let the 29 changed files scare you, most of those are just the test fixtures :) # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> This is the current behavior with prefix matching: ![image](https://github.com/nushell/nushell/assets/45539777/6a36e003-8405-45b5-8cbe-d771e0592709) And with fuzzy matching: ![image](https://github.com/nushell/nushell/assets/45539777/f2cbfdb2-b8fd-491b-a378-779147291d2a) Notice how `partial/hello.txt` is the last suggestion, even though it should come before `partial-a`. This is because the ASCII code for `/` is greater than that of `-`, so `partial-` is put before `partial/`. This is this PR's behavior with prefix matching (`partial/hello.txt` is at the start): ![image](https://github.com/nushell/nushell/assets/45539777/3fcea7c9-e017-428f-aa9c-1707e3ab32e0) And with fuzzy matching: ![image](https://github.com/nushell/nushell/assets/45539777/d55635d4-cdb8-440a-84d6-41111499f9f8) # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> - Modified the partial completions test fixture to test whether this PR even fixed anything - Modified fixture to test sort order of .nu completions (a previous version of my changes didn't sort all the completions at the end but there were no tests catching that) - Added a test for making sure subcommand completions are sorted by Levenshtein distance (a previous version of my changes sorted in alphabetical order but there were no tests catching that) # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
251 lines
6.9 KiB
Rust
251 lines
6.9 KiB
Rust
use nu_engine::eval_block;
|
|
use nu_parser::parse;
|
|
use nu_protocol::{
|
|
debugger::WithoutDebug,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
PipelineData, ShellError, Span, Value,
|
|
};
|
|
use nu_test_support::fs;
|
|
use reedline::Suggestion;
|
|
use std::path::{PathBuf, MAIN_SEPARATOR};
|
|
|
|
fn create_default_context() -> EngineState {
|
|
nu_command::add_shell_command_context(nu_cmd_lang::create_default_context())
|
|
}
|
|
|
|
// creates a new engine with the current path into the completions fixtures folder
|
|
pub fn new_engine() -> (PathBuf, String, EngineState, Stack) {
|
|
// Target folder inside assets
|
|
let dir = fs::fixtures().join("completions");
|
|
let dir_str = dir
|
|
.clone()
|
|
.into_os_string()
|
|
.into_string()
|
|
.unwrap_or_default();
|
|
|
|
// Create a new engine with default context
|
|
let mut engine_state = create_default_context();
|
|
|
|
// Add $nu
|
|
engine_state.generate_nu_constant();
|
|
|
|
// New stack
|
|
let mut stack = Stack::new();
|
|
|
|
// Add pwd as env var
|
|
stack.add_env_var(
|
|
"PWD".to_string(),
|
|
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())),
|
|
);
|
|
stack.add_env_var(
|
|
"TEST".to_string(),
|
|
Value::string(
|
|
"NUSHELL".to_string(),
|
|
nu_protocol::Span::new(0, dir_str.len()),
|
|
),
|
|
);
|
|
#[cfg(windows)]
|
|
stack.add_env_var(
|
|
"Path".to_string(),
|
|
Value::string(
|
|
"c:\\some\\path;c:\\some\\other\\path".to_string(),
|
|
nu_protocol::Span::new(0, dir_str.len()),
|
|
),
|
|
);
|
|
#[cfg(not(windows))]
|
|
stack.add_env_var(
|
|
"PATH".to_string(),
|
|
Value::string(
|
|
"/some/path:/some/other/path".to_string(),
|
|
nu_protocol::Span::new(0, dir_str.len()),
|
|
),
|
|
);
|
|
|
|
// Merge environment into the permanent state
|
|
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
|
assert!(merge_result.is_ok());
|
|
|
|
(dir, dir_str, engine_state, stack)
|
|
}
|
|
|
|
// creates a new engine with the current path into the completions fixtures folder
|
|
pub fn new_dotnu_engine() -> (PathBuf, String, EngineState, Stack) {
|
|
// Target folder inside assets
|
|
let dir = fs::fixtures().join("dotnu_completions");
|
|
let dir_str = dir
|
|
.clone()
|
|
.into_os_string()
|
|
.into_string()
|
|
.unwrap_or_default();
|
|
let dir_span = nu_protocol::Span::new(0, dir_str.len());
|
|
|
|
// Create a new engine with default context
|
|
let mut engine_state = create_default_context();
|
|
|
|
// Add $nu
|
|
engine_state.generate_nu_constant();
|
|
|
|
// New stack
|
|
let mut stack = Stack::new();
|
|
|
|
// Add pwd as env var
|
|
stack.add_env_var("PWD".to_string(), Value::string(dir_str.clone(), dir_span));
|
|
stack.add_env_var(
|
|
"TEST".to_string(),
|
|
Value::string("NUSHELL".to_string(), dir_span),
|
|
);
|
|
|
|
stack.add_env_var(
|
|
"NU_LIB_DIRS".to_string(),
|
|
Value::List {
|
|
vals: vec![
|
|
Value::string(file(dir.join("lib-dir1")), dir_span),
|
|
Value::string(file(dir.join("lib-dir2")), dir_span),
|
|
Value::string(file(dir.join("lib-dir3")), dir_span),
|
|
],
|
|
internal_span: dir_span,
|
|
},
|
|
);
|
|
|
|
// Merge environment into the permanent state
|
|
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
|
assert!(merge_result.is_ok());
|
|
|
|
(dir, dir_str, engine_state, stack)
|
|
}
|
|
|
|
pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) {
|
|
// Target folder inside assets
|
|
let dir = fs::fixtures().join("quoted_completions");
|
|
let dir_str = dir
|
|
.clone()
|
|
.into_os_string()
|
|
.into_string()
|
|
.unwrap_or_default();
|
|
|
|
// Create a new engine with default context
|
|
let mut engine_state = create_default_context();
|
|
|
|
// New stack
|
|
let mut stack = Stack::new();
|
|
|
|
// Add pwd as env var
|
|
stack.add_env_var(
|
|
"PWD".to_string(),
|
|
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())),
|
|
);
|
|
stack.add_env_var(
|
|
"TEST".to_string(),
|
|
Value::string(
|
|
"NUSHELL".to_string(),
|
|
nu_protocol::Span::new(0, dir_str.len()),
|
|
),
|
|
);
|
|
|
|
// Merge environment into the permanent state
|
|
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
|
assert!(merge_result.is_ok());
|
|
|
|
(dir, dir_str, engine_state, stack)
|
|
}
|
|
|
|
pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) {
|
|
// Target folder inside assets
|
|
let dir = fs::fixtures().join("partial_completions");
|
|
let dir_str = dir
|
|
.clone()
|
|
.into_os_string()
|
|
.into_string()
|
|
.unwrap_or_default();
|
|
|
|
// Create a new engine with default context
|
|
let mut engine_state = create_default_context();
|
|
|
|
// New stack
|
|
let mut stack = Stack::new();
|
|
|
|
// Add pwd as env var
|
|
stack.add_env_var(
|
|
"PWD".to_string(),
|
|
Value::string(dir_str.clone(), nu_protocol::Span::new(0, dir_str.len())),
|
|
);
|
|
stack.add_env_var(
|
|
"TEST".to_string(),
|
|
Value::string(
|
|
"NUSHELL".to_string(),
|
|
nu_protocol::Span::new(0, dir_str.len()),
|
|
),
|
|
);
|
|
|
|
// Merge environment into the permanent state
|
|
let merge_result = engine_state.merge_env(&mut stack, &dir);
|
|
assert!(merge_result.is_ok());
|
|
|
|
(dir, dir_str, engine_state, stack)
|
|
}
|
|
|
|
// match a list of suggestions with the expected values
|
|
pub fn match_suggestions(expected: Vec<String>, suggestions: Vec<Suggestion>) {
|
|
let expected_len = expected.len();
|
|
let suggestions_len = suggestions.len();
|
|
if expected_len != suggestions_len {
|
|
panic!(
|
|
"\nexpected {expected_len} suggestions but got {suggestions_len}: \n\
|
|
Suggestions: {suggestions:#?} \n\
|
|
Expected: {expected:#?}\n"
|
|
)
|
|
}
|
|
assert_eq!(
|
|
expected,
|
|
suggestions
|
|
.into_iter()
|
|
.map(|it| it.value)
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
// append the separator to the converted path
|
|
pub fn folder(path: PathBuf) -> String {
|
|
let mut converted_path = file(path);
|
|
converted_path.push(MAIN_SEPARATOR);
|
|
|
|
converted_path
|
|
}
|
|
|
|
// convert a given path to string
|
|
pub fn file(path: PathBuf) -> String {
|
|
path.into_os_string().into_string().unwrap_or_default()
|
|
}
|
|
|
|
// merge_input executes the given input into the engine
|
|
// and merges the state
|
|
pub fn merge_input(
|
|
input: &[u8],
|
|
engine_state: &mut EngineState,
|
|
stack: &mut Stack,
|
|
dir: PathBuf,
|
|
) -> Result<(), ShellError> {
|
|
let (block, delta) = {
|
|
let mut working_set = StateWorkingSet::new(engine_state);
|
|
|
|
let block = parse(&mut working_set, None, input, false);
|
|
|
|
assert!(working_set.parse_errors.is_empty());
|
|
|
|
(block, working_set.render())
|
|
};
|
|
|
|
engine_state.merge_delta(delta)?;
|
|
|
|
assert!(eval_block::<WithoutDebug>(
|
|
engine_state,
|
|
stack,
|
|
&block,
|
|
PipelineData::Value(Value::nothing(Span::unknown()), None),
|
|
)
|
|
.is_ok());
|
|
|
|
// Merge environment into the permanent state
|
|
engine_state.merge_env(stack, &dir)
|
|
}
|