colored file-like completions (#11702)

<!--
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.
-->

`ls` and other file completions uses `LS_COLORS`.

![maim-2024 01 31 21 34
31](https://github.com/nushell/nushell/assets/15631555/d5c3813f-77b5-4391-aa0b-4b2125e5aca5)


# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# 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 std testing; testing run-tests --path
crates/nu-std"` 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
> ```
-->

# 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.
-->

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Steven 2024-02-08 12:29:28 -08:00 committed by GitHub
parent bacc1f6317
commit 5042f19d1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 107 additions and 36 deletions

1
Cargo.lock generated
View file

@ -2843,6 +2843,7 @@ dependencies = [
"fuzzy-matcher", "fuzzy-matcher",
"is_executable", "is_executable",
"log", "log",
"lscolors",
"miette", "miette",
"nu-ansi-term", "nu-ansi-term",
"nu-cmd-base", "nu-cmd-base",

View file

@ -34,6 +34,7 @@ fuzzy-matcher = "0.3"
is_executable = "1.0" is_executable = "1.0"
log = "0.4" log = "0.4"
miette = { version = "7.0", features = ["fancy-no-backtrace"] } miette = { version = "7.0", features = ["fancy-no-backtrace"] }
lscolors = { version = "0.17", default-features = false, features = ["nu-ansi-term"] }
once_cell = "1.18" once_cell = "1.18"
percent-encoding = "2" percent-encoding = "2"
pathdiff = "0.2" pathdiff = "0.2"

View file

@ -266,8 +266,10 @@ impl NuCompleter {
|| prev_expr_str == b"overlay use" || prev_expr_str == b"overlay use"
|| prev_expr_str == b"source-env" || prev_expr_str == b"source-env"
{ {
let mut completer = let mut completer = DotNuCompletion::new(
DotNuCompletion::new(self.engine_state.clone()); self.engine_state.clone(),
self.stack.clone(),
);
return self.process_completion( return self.process_completion(
&mut completer, &mut completer,
@ -278,8 +280,10 @@ impl NuCompleter {
pos, pos,
); );
} else if prev_expr_str == b"ls" { } else if prev_expr_str == b"ls" {
let mut completer = let mut completer = FileCompletion::new(
FileCompletion::new(self.engine_state.clone()); self.engine_state.clone(),
self.stack.clone(),
);
return self.process_completion( return self.process_completion(
&mut completer, &mut completer,
@ -313,8 +317,10 @@ impl NuCompleter {
); );
} }
FlatShape::Directory => { FlatShape::Directory => {
let mut completer = let mut completer = DirectoryCompletion::new(
DirectoryCompletion::new(self.engine_state.clone()); self.engine_state.clone(),
self.stack.clone(),
);
return self.process_completion( return self.process_completion(
&mut completer, &mut completer,
@ -326,8 +332,10 @@ impl NuCompleter {
); );
} }
FlatShape::Filepath | FlatShape::GlobPattern => { FlatShape::Filepath | FlatShape::GlobPattern => {
let mut completer = let mut completer = FileCompletion::new(
FileCompletion::new(self.engine_state.clone()); self.engine_state.clone(),
self.stack.clone(),
);
return self.process_completion( return self.process_completion(
&mut completer, &mut completer,
@ -374,8 +382,10 @@ impl NuCompleter {
} }
// Check for file completion // Check for file completion
let mut completer = let mut completer = FileCompletion::new(
FileCompletion::new(self.engine_state.clone()); self.engine_state.clone(),
self.stack.clone(),
);
out = self.process_completion( out = self.process_completion(
&mut completer, &mut completer,
&working_set, &working_set,

View file

@ -1,6 +1,10 @@
use crate::completions::{matches, CompletionOptions}; use crate::completions::{matches, CompletionOptions};
use nu_ansi_term::Style;
use nu_engine::env_to_string;
use nu_path::home_dir; use nu_path::home_dir;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{engine::StateWorkingSet, Span}; use nu_protocol::{engine::StateWorkingSet, Span};
use nu_utils::get_ls_colors;
use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP}; use std::path::{is_separator, Component, Path, PathBuf, MAIN_SEPARATOR as SEP};
fn complete_rec( fn complete_rec(
@ -92,10 +96,21 @@ pub fn complete_item(
partial: &str, partial: &str,
cwd: &str, cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<(nu_protocol::Span, String)> { engine_state: &EngineState,
stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
let partial = surround_remove(partial); let partial = surround_remove(partial);
let isdir = partial.ends_with(is_separator); let isdir = partial.ends_with(is_separator);
let cwd_pathbuf = Path::new(cwd).to_path_buf(); let cwd_pathbuf = Path::new(cwd).to_path_buf();
let ls_colors = (engine_state.config.use_ls_colors_completions
&& engine_state.config.use_ansi_coloring)
.then(|| {
let ls_colors_env_str = match stack.get_env_var(engine_state, "LS_COLORS") {
Some(v) => env_to_string("LS_COLORS", &v, engine_state, stack).ok(),
None => None,
};
get_ls_colors(ls_colors_env_str)
});
let mut original_cwd = OriginalCwd::None; let mut original_cwd = OriginalCwd::None;
let mut components = Path::new(&partial).components().peekable(); let mut components = Path::new(&partial).components().peekable();
let mut cwd = match components.peek().cloned() { let mut cwd = match components.peek().cloned() {
@ -148,7 +163,18 @@ pub fn complete_item(
complete_rec(partial.as_slice(), &cwd, options, want_directory, isdir) complete_rec(partial.as_slice(), &cwd, options, want_directory, isdir)
.into_iter() .into_iter()
.map(|p| (span, escape_path(original_cwd.apply(&p), want_directory))) .map(|p| {
let path = original_cwd.apply(&p);
let style = ls_colors.as_ref().map(|lsc| {
lsc.style_for_path_with_metadata(
&path,
std::fs::symlink_metadata(&path).ok().as_ref(),
)
.map(lscolors::Style::to_nu_ansi_term_style)
.unwrap_or_default()
});
(span, escape_path(path, want_directory), style)
})
.collect() .collect()
} }

View file

@ -2,8 +2,9 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, SortBy, Completer, CompletionOptions, SortBy,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
levenshtein_distance, Span, levenshtein_distance, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -13,11 +14,15 @@ use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct DirectoryCompletion { pub struct DirectoryCompletion {
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
stack: Stack,
} }
impl DirectoryCompletion { impl DirectoryCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self { engine_state } Self {
engine_state,
stack,
}
} }
} }
@ -39,12 +44,14 @@ impl Completer for DirectoryCompletion {
&prefix, &prefix,
&self.engine_state.current_work_dir(), &self.engine_state.current_work_dir(),
options, options,
self.engine_state.as_ref(),
&self.stack,
) )
.into_iter() .into_iter()
.map(move |x| Suggestion { .map(move |x| Suggestion {
value: x.1, value: x.1,
description: None, description: None,
style: None, style: x.2,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.0.start - offset,
@ -113,6 +120,8 @@ pub fn directory_completion(
partial: &str, partial: &str,
cwd: &str, cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<(nu_protocol::Span, String)> { engine_state: &EngineState,
complete_item(true, span, partial, cwd, options) stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(true, span, partial, cwd, options, engine_state, stack)
} }

View file

@ -1,6 +1,6 @@
use crate::completions::{file_path_completion, Completer, CompletionOptions, SortBy}; use crate::completions::{file_path_completion, Completer, CompletionOptions, SortBy};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
Span, Span,
}; };
use reedline::Suggestion; use reedline::Suggestion;
@ -12,11 +12,15 @@ use std::{
#[derive(Clone)] #[derive(Clone)]
pub struct DotNuCompletion { pub struct DotNuCompletion {
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
stack: Stack,
} }
impl DotNuCompletion { impl DotNuCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self { engine_state } Self {
engine_state,
stack,
}
} }
} }
@ -92,7 +96,14 @@ impl Completer for DotNuCompletion {
let output: Vec<Suggestion> = search_dirs let output: Vec<Suggestion> = search_dirs
.into_iter() .into_iter()
.flat_map(|search_dir| { .flat_map(|search_dir| {
let completions = file_path_completion(span, &partial, &search_dir, options); let completions = file_path_completion(
span,
&partial,
&search_dir,
options,
self.engine_state.as_ref(),
&self.stack,
);
completions completions
.into_iter() .into_iter()
.filter(move |it| { .filter(move |it| {
@ -111,7 +122,7 @@ impl Completer for DotNuCompletion {
.map(move |x| Suggestion { .map(move |x| Suggestion {
value: x.1, value: x.1,
description: None, description: None,
style: None, style: x.2,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.0.start - offset,

View file

@ -2,8 +2,9 @@ use crate::completions::{
completion_common::{adjust_if_intermediate, complete_item, AdjustView}, completion_common::{adjust_if_intermediate, complete_item, AdjustView},
Completer, CompletionOptions, SortBy, Completer, CompletionOptions, SortBy,
}; };
use nu_ansi_term::Style;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
levenshtein_distance, Span, levenshtein_distance, Span,
}; };
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
@ -14,11 +15,15 @@ use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct FileCompletion { pub struct FileCompletion {
engine_state: Arc<EngineState>, engine_state: Arc<EngineState>,
stack: Stack,
} }
impl FileCompletion { impl FileCompletion {
pub fn new(engine_state: Arc<EngineState>) -> Self { pub fn new(engine_state: Arc<EngineState>, stack: Stack) -> Self {
Self { engine_state } Self {
engine_state,
stack,
}
} }
} }
@ -44,12 +49,14 @@ impl Completer for FileCompletion {
&prefix, &prefix,
&self.engine_state.current_work_dir(), &self.engine_state.current_work_dir(),
options, options,
self.engine_state.as_ref(),
&self.stack,
) )
.into_iter() .into_iter()
.map(move |x| Suggestion { .map(move |x| Suggestion {
value: x.1, value: x.1,
description: None, description: None,
style: None, style: x.2,
extra: None, extra: None,
span: reedline::Span { span: reedline::Span {
start: x.0.start - offset, start: x.0.start - offset,
@ -118,8 +125,10 @@ pub fn file_path_completion(
partial: &str, partial: &str,
cwd: &str, cwd: &str,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<(nu_protocol::Span, String)> { engine_state: &EngineState,
complete_item(false, span, partial, cwd, options) stack: &Stack,
) -> Vec<(nu_protocol::Span, String, Option<Style>)> {
complete_item(false, span, partial, cwd, options, engine_state, stack)
} }
pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool { pub fn matches(partial: &str, from: &str, options: &CompletionOptions) -> bool {

View file

@ -897,13 +897,10 @@ fn render_path_name(
let stripped_path = nu_utils::strip_ansi_unlikely(path); let stripped_path = nu_utils::strip_ansi_unlikely(path);
let (style, has_metadata) = match std::fs::symlink_metadata(stripped_path.as_ref()) { let metadata = std::fs::symlink_metadata(stripped_path.as_ref());
Ok(metadata) => ( let has_metadata = metadata.is_ok();
ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), Some(&metadata)), let style =
true, ls_colors.style_for_path_with_metadata(stripped_path.as_ref(), metadata.ok().as_ref());
),
Err(_) => (ls_colors.style_for_path(stripped_path.as_ref()), false),
};
// clickable links don't work in remote SSH sessions // clickable links don't work in remote SSH sessions
let in_ssh_session = std::env::var("SSH_CLIENT").is_ok(); let in_ssh_session = std::env::var("SSH_CLIENT").is_ok();

View file

@ -89,6 +89,7 @@ pub struct Config {
pub error_style: ErrorStyle, pub error_style: ErrorStyle,
pub use_kitty_protocol: bool, pub use_kitty_protocol: bool,
pub highlight_resolved_externals: bool, pub highlight_resolved_externals: bool,
pub use_ls_colors_completions: bool,
/// Configuration for plugins. /// Configuration for plugins.
/// ///
/// Users can provide configuration for a plugin through this entry. The entry name must /// Users can provide configuration for a plugin through this entry. The entry name must
@ -129,6 +130,7 @@ impl Default for Config {
enable_external_completion: true, enable_external_completion: true,
max_external_completion_results: 100, max_external_completion_results: 100,
external_completer: None, external_completer: None,
use_ls_colors_completions: false,
filesize_metric: false, filesize_metric: false,
filesize_format: "auto".into(), filesize_format: "auto".into(),
@ -349,6 +351,9 @@ impl Value {
*value = reconstruct_external(&config, span); *value = reconstruct_external(&config, span);
} }
} }
"use_ls_colors" => {
process_bool_config(value, &mut errors, &mut config.use_ls_colors_completions);
}
_ => { _ => {
report_invalid_key(&[key, key2], span, &mut errors); report_invalid_key(&[key, key2], span, &mut errors);
return false; return false;
@ -366,6 +371,7 @@ impl Value {
"algorithm" => config.completion_algorithm.reconstruct_value(span), "algorithm" => config.completion_algorithm.reconstruct_value(span),
"case_sensitive" => Value::bool(config.case_sensitive_completions, span), "case_sensitive" => Value::bool(config.case_sensitive_completions, span),
"external" => reconstruct_external(&config, span), "external" => reconstruct_external(&config, span),
"use_ls_colors" => Value::bool(config.use_ls_colors_completions, span),
}, },
span, span,
); );

View file

@ -212,6 +212,7 @@ $env.config = {
max_results: 100 # setting it lower can improve completion performance at the cost of omitting some options max_results: 100 # setting it lower can improve completion performance at the cost of omitting some options
completer: null # check 'carapace_completer' above as an example completer: null # check 'carapace_completer' above as an example
} }
use_ls_colors: false # set this to true to enable file/path/directory completions using LS_COLORS
} }
filesize: { filesize: {