From 5c2bcd068b02183ec7528fe1d6cd26e57abd25f2 Mon Sep 17 00:00:00 2001 From: Yash Thakur <45539777+ysthakur@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:19:09 -0400 Subject: [PATCH 01/17] Enable exact match behavior for any path with slashes (#15458) # Description Closes #14794. This PR enables the strict exact match behavior requested in #13204 and #14794 for any path containing a slash (#13302 implemented this for paths ending in slashes). If any of the components along the way *don't* exactly match a directory, then the next components will use the old Fish-like completion behavior rather than the strict behavior. This change only affects those using prefix matching. Fuzzy matching remains unaffected. # User-Facing Changes Suppose you have the following directory structure: ``` - foo - bar - xyzzy - barbaz - xyzzy - foobar - bar - xyzzy - barbaz - xyzzy ``` - If you type `cd foo`, you will be suggested `[foo, foobar]` - This is because `foo` is the last component of the path, so the strict behavior isn't activated - Similarly, `foo/bar` will show you `[foo/bar, foo/barbaz]` - If you type `foo/bar/x`, you will be suggested `[foo/bar/xyzzy]` - This is because `foo` and `bar` both exactly matched a directory - If you type `foo/b/x`, you will be suggested `[foo/bar/xyzzy, foo/barbaz/xyzzy]` - This is because `foo` matches a directory exactly, so `foobar/*` won't be suggested, but `b` doesn't exactly match a directory, so both `bar` and `barbaz` are suggested - If you type `f/b/x`, you will be suggested all four of the `xyzzy` files above - If you type `f/bar/x`, you will be suggested all four of the `xyzzy` files above - Since `f` doesn't exactly match a directory, every component after it won't use the strict matching behavior (even though `bar` exactly matches a directory) # Tests + Formatting # After Submitting This is a pretty minor change but should be mentioned somewhere in the release notes in case it surprises someone. --------- Co-authored-by: 132ikl <132@ikl.sh> --- .../src/completions/completion_common.rs | 41 +++++++++++------- crates/nu-cli/tests/completions/mod.rs | 42 ++++++++++++++++--- .../partial-a/{hola => hola/foo.txt} | 0 .../partial_completions/partial/hol/foo.txt | 0 4 files changed, 62 insertions(+), 21 deletions(-) rename tests/fixtures/partial_completions/partial-a/{hola => hola/foo.txt} (100%) create mode 100644 tests/fixtures/partial_completions/partial/hol/foo.txt diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index c40a57527b..e8179e0ec9 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -22,18 +22,22 @@ pub struct PathBuiltFromString { /// Recursively goes through paths that match a given `partial`. /// built: State struct for a valid matching path built so far. /// +/// `want_directory`: Whether we want only directories as completion matches. +/// Some commands like `cd` can only be run on directories whereas others +/// like `ls` can be run on regular files as well. +/// /// `isdir`: whether the current partial path has a trailing slash. /// Parsing a path string into a pathbuf loses that bit of information. /// -/// want_directory: Whether we want only directories as completion matches. -/// Some commands like `cd` can only be run on directories whereas others -/// like `ls` can be run on regular files as well. +/// `enable_exact_match`: Whether match algorithm is Prefix and all previous components +/// of the path matched a directory exactly. fn complete_rec( partial: &[&str], built_paths: &[PathBuiltFromString], options: &CompletionOptions, want_directory: bool, isdir: bool, + enable_exact_match: bool, ) -> Vec { if let Some((&base, rest)) = partial.split_first() { if base.chars().all(|c| c == '.') && (isdir || !rest.is_empty()) { @@ -46,7 +50,14 @@ fn complete_rec( built }) .collect(); - return complete_rec(rest, &built_paths, options, want_directory, isdir); + return complete_rec( + rest, + &built_paths, + options, + want_directory, + isdir, + enable_exact_match, + ); } } @@ -86,27 +97,26 @@ fn complete_rec( // Serves as confirmation to ignore longer completions for // components in between. if !rest.is_empty() || isdir { + // Don't show longer completions if we have an exact match (#13204, #14794) + let exact_match = enable_exact_match + && (if options.case_sensitive { + entry_name.eq(base) + } else { + entry_name.eq_ignore_case(base) + }); completions.extend(complete_rec( rest, &[built], options, want_directory, isdir, + exact_match, )); - } else { - completions.push(built); - } - - // For https://github.com/nushell/nushell/issues/13204 - if isdir && options.match_algorithm == MatchAlgorithm::Prefix { - let exact_match = if options.case_sensitive { - entry_name.eq(base) - } else { - entry_name.to_folded_case().eq(&base.to_folded_case()) - }; if exact_match { break; } + } else { + completions.push(built); } } None => { @@ -255,6 +265,7 @@ pub fn complete_item( options, want_directory, isdir, + options.match_algorithm == MatchAlgorithm::Prefix, ) .into_iter() .map(|mut p| { diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 10a207b88c..e2b481f676 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -951,10 +951,11 @@ fn partial_completions() { // Create the expected values let expected_paths = [ file(dir.join("partial").join("hello.txt")), + folder(dir.join("partial").join("hol")), file(dir.join("partial-a").join("have_ext.exe")), file(dir.join("partial-a").join("have_ext.txt")), file(dir.join("partial-a").join("hello")), - file(dir.join("partial-a").join("hola")), + folder(dir.join("partial-a").join("hola")), file(dir.join("partial-b").join("hello_b")), file(dir.join("partial-b").join("hi_b")), file(dir.join("partial-c").join("hello_c")), @@ -971,11 +972,12 @@ fn partial_completions() { // Create the expected values let expected_paths = [ file(dir.join("partial").join("hello.txt")), + folder(dir.join("partial").join("hol")), file(dir.join("partial-a").join("anotherfile")), file(dir.join("partial-a").join("have_ext.exe")), file(dir.join("partial-a").join("have_ext.txt")), file(dir.join("partial-a").join("hello")), - file(dir.join("partial-a").join("hola")), + folder(dir.join("partial-a").join("hola")), file(dir.join("partial-b").join("hello_b")), file(dir.join("partial-b").join("hi_b")), file(dir.join("partial-c").join("hello_c")), @@ -2215,15 +2217,43 @@ fn exact_match() { let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + // Troll case to test if exact match logic works case insensitively let target_dir = format!("open {}", folder(dir.join("pArTiAL"))); let suggestions = completer.complete(&target_dir, target_dir.len()); - - // Since it's an exact match, only 'partial' should be suggested, not - // 'partial-a' and stuff. Implemented in #13302 match_suggestions( - &vec![file(dir.join("partial").join("hello.txt")).as_str()], + &vec![ + file(dir.join("partial").join("hello.txt")).as_str(), + folder(dir.join("partial").join("hol")).as_str(), + ], &suggestions, ); + + let target_dir = format!("open {}", file(dir.join("partial").join("h"))); + let suggestions = completer.complete(&target_dir, target_dir.len()); + match_suggestions( + &vec![ + file(dir.join("partial").join("hello.txt")).as_str(), + folder(dir.join("partial").join("hol")).as_str(), + ], + &suggestions, + ); + + // Even though "hol" is an exact match, the first component ("part") wasn't an + // exact match, so we include partial-a/hola + let target_dir = format!("open {}", file(dir.join("part").join("hol"))); + let suggestions = completer.complete(&target_dir, target_dir.len()); + match_suggestions( + &vec![ + folder(dir.join("partial").join("hol")).as_str(), + folder(dir.join("partial-a").join("hola")).as_str(), + ], + &suggestions, + ); + + // Exact match behavior shouldn't be enabled if the path has no slashes + let target_dir = format!("open {}", file(dir.join("partial"))); + let suggestions = completer.complete(&target_dir, target_dir.len()); + assert!(suggestions.len() > 1); } #[ignore = "was reverted, still needs fixing"] diff --git a/tests/fixtures/partial_completions/partial-a/hola b/tests/fixtures/partial_completions/partial-a/hola/foo.txt similarity index 100% rename from tests/fixtures/partial_completions/partial-a/hola rename to tests/fixtures/partial_completions/partial-a/hola/foo.txt diff --git a/tests/fixtures/partial_completions/partial/hol/foo.txt b/tests/fixtures/partial_completions/partial/hol/foo.txt new file mode 100644 index 0000000000..e69de29bb2 From ca4222277ebabeb66d149d608539ea505b0b4178 Mon Sep 17 00:00:00 2001 From: migraine-user Date: Mon, 31 Mar 2025 21:38:50 +0200 Subject: [PATCH 02/17] Fix typo in doc_config.nu + small description (#15461) # Description ``` # table.* # table_mode (string): # One of: "default", "basic", "compact", "compact_double", "heavy", "light", "none", "reinforced", # "rounded", "thin", "with_love", "psql", "markdown", "dots", "restructured", "ascii_rounded", # or "basic_compact" # Can be overridden by passing a table to `| table --theme/-t` $env.config.table.mode = "default" ``` In `doc_config.nu`, it refers to `table_mode` which does not exist under `$env.config.table`. There is now a short description of this field as well. --- crates/nu-utils/src/default_files/doc_config.nu | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/nu-utils/src/default_files/doc_config.nu b/crates/nu-utils/src/default_files/doc_config.nu index 33296bc9c2..874628cf2c 100644 --- a/crates/nu-utils/src/default_files/doc_config.nu +++ b/crates/nu-utils/src/default_files/doc_config.nu @@ -295,7 +295,8 @@ $env.config.display_errors.termination_signal = true $env.config.footer_mode = 25 # table.* -# table_mode (string): +# mode (string): +# Specifies the visual display style of a table # One of: "default", "basic", "compact", "compact_double", "heavy", "light", "none", "reinforced", # "rounded", "thin", "with_love", "psql", "markdown", "dots", "restructured", "ascii_rounded", # or "basic_compact" From 1dcaffb79272b87f84a4b103f361d0fda6f8af9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:03:03 +0800 Subject: [PATCH 03/17] build(deps): bump array-init-cursor from 0.2.0 to 0.2.1 (#15460) Bumps [array-init-cursor](https://github.com/planus-org/planus) from 0.2.0 to 0.2.1.
Changelog

Sourced from array-init-cursor's changelog.

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

Fixed

Removed

[1.1.1] - 2025-03-02

Added

Fixed

  • [Rust]: Fix the alignment of structs in unions #289

Removed

[1.1.0] - 2025-03-02

Added

  • Bump the Minimum Support Rust Version (MSRV) to 1.75.0
  • The Primitive and VectorWrite traits are now marked as unsafe to remind implementers of alignment constraints
  • [Rust]: Add support for union vectors #287
  • Add support for displaying union vectors with planus view #287

Fixed

  • Added extra unsafe blocks to templates to fix warnings for the 2024 edition
  • Updated tests for the 2024 edition

Removed

[1.0.0] - 2024-09-29

Added

  • [Rust]: Added #[allow(dead_code)] to the root of the generated rust code #204
  • Added the option ignore_docstring_errors to the app. #216
  • Get rid of dependency on atty and bump the Minimum Support Rust Version (MSRV) to 1.70.0. #220
  • [Rust]: Allow default implementations to be generated for tables that have fields with (required) vectors, strings, integers and bools. #243

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=array-init-cursor&package-manager=cargo&previous-version=0.2.0&new-version=0.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/nushell/nushell/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5ac70089f..87a35ff52a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,9 +206,9 @@ dependencies = [ [[package]] name = "array-init-cursor" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7d0a018de4f6aa429b9d33d69edf69072b1c5b1cb8d3e4a5f7ef898fc3eb76" +checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" [[package]] name = "arrayref" From 6c0b65b57054f0319ea310b961b4c2b51a659ed7 Mon Sep 17 00:00:00 2001 From: zc he Date: Tue, 1 Apr 2025 20:13:07 +0800 Subject: [PATCH 04/17] feat(completion): stdlib virtual path completion & exportable completion (#15270) # Description More completions for `use` command. ~Also optimizes the span fix of #15238 to allow changing the text after the cursor.~ # User-Facing Changes image image # Tests + Formatting +3 # After Submitting --- Cargo.lock | 2 + crates/nu-cli/Cargo.toml | 1 + crates/nu-cli/src/completions/completer.rs | 134 +++++++++++++++--- .../src/completions/completion_common.rs | 2 +- .../src/completions/dotnu_completions.rs | 68 +++++++-- .../src/completions/exportable_completions.rs | 111 +++++++++++++++ .../src/completions/flag_completions.rs | 6 +- crates/nu-cli/src/completions/mod.rs | 2 + crates/nu-cli/tests/completions/mod.rs | 64 ++++++++- crates/nu-lsp/Cargo.toml | 1 + crates/nu-lsp/src/completion.rs | 117 ++++++++++++--- crates/nu-lsp/src/lib.rs | 2 + tests/fixtures/lsp/completion/use.nu | 8 ++ 13 files changed, 470 insertions(+), 48 deletions(-) create mode 100644 crates/nu-cli/src/completions/exportable_completions.rs create mode 100644 tests/fixtures/lsp/completion/use.nu diff --git a/Cargo.lock b/Cargo.lock index 87a35ff52a..5bdaf61196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3542,6 +3542,7 @@ dependencies = [ "nu-path", "nu-plugin-engine", "nu-protocol", + "nu-std", "nu-test-support", "nu-utils", "nucleo-matcher", @@ -3827,6 +3828,7 @@ dependencies = [ "nu-glob", "nu-parser", "nu-protocol", + "nu-std", "nu-test-support", "nu-utils", "nucleo-matcher", diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index f3bfb0f91c..0909caf125 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -13,6 +13,7 @@ bench = false [dev-dependencies] nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" } nu-command = { path = "../nu-command", version = "0.103.1" } +nu-std = { path = "../nu-std", version = "0.103.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.1" } rstest = { workspace = true, default-features = false } tempfile = { workspace = true } diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index a7a00306b0..bb56963942 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -1,21 +1,20 @@ use crate::completions::{ + base::{SemanticSuggestion, SuggestionKind}, AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer, - CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, - FlagCompletion, OperatorCompletion, VariableCompletion, + CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, + ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion, }; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; use nu_engine::eval_block; -use nu_parser::{flatten_expression, parse}; +use nu_parser::{flatten_expression, parse, parse_module_file_or_dir}; use nu_protocol::{ - ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse}, + ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse}, debugger::WithoutDebug, engine::{Closure, EngineState, Stack, StateWorkingSet}, PipelineData, Span, Type, Value, }; use reedline::{Completer as ReedlineCompleter, Suggestion}; -use std::{str, sync::Arc}; - -use super::base::{SemanticSuggestion, SuggestionKind}; +use std::sync::Arc; /// Used as the function `f` in find_map Traverse /// @@ -57,8 +56,13 @@ fn find_pipeline_element_by_position<'a>( Expr::FullCellPath(fcp) => fcp .head .find_map(working_set, &closure) - .or(Some(expr)) .map(FindMapResult::Found) + // e.g. use std/util [ + .or_else(|| { + (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_))) + .then_some(FindMapResult::Continue) + }) + .or(Some(FindMapResult::Found(expr))) .unwrap_or_default(), Expr::Var(_) => FindMapResult::Found(expr), Expr::AttributeBlock(ab) => ab @@ -127,6 +131,18 @@ struct Context<'a> { offset: usize, } +/// For argument completion +struct PositionalArguments<'a> { + /// command name + command_head: &'a [u8], + /// indices of positional arguments + positional_arg_indices: Vec, + /// argument list + arguments: &'a [Argument], + /// expression of current argument + expr: &'a Expression, +} + impl Context<'_> { fn new<'a>( working_set: &'a StateWorkingSet, @@ -328,7 +344,8 @@ impl NuCompleter { // NOTE: the argument to complete is not necessarily the last one // for lsp completion, we don't trim the text, // so that `def`s after pos can be completed - for arg in call.arguments.iter() { + let mut positional_arg_indices = Vec::new(); + for (arg_idx, arg) in call.arguments.iter().enumerate() { let span = arg.span(); if span.contains(pos) { // if customized completion specified, it has highest priority @@ -379,9 +396,15 @@ impl NuCompleter { // complete according to expression type and command head Argument::Positional(expr) => { let command_head = working_set.get_span_contents(call.head); + positional_arg_indices.push(arg_idx); self.argument_completion_helper( - command_head, - expr, + PositionalArguments { + command_head, + positional_arg_indices, + arguments: &call.arguments, + expr, + }, + pos, &ctx, suggestions.is_empty(), ) @@ -389,6 +412,8 @@ impl NuCompleter { _ => vec![], }); break; + } else if !matches!(arg, Argument::Named(_)) { + positional_arg_indices.push(arg_idx); } } } @@ -498,18 +523,95 @@ impl NuCompleter { fn argument_completion_helper( &self, - command_head: &[u8], - expr: &Expression, + argument_info: PositionalArguments, + pos: usize, ctx: &Context, need_fallback: bool, ) -> Vec { + let PositionalArguments { + command_head, + positional_arg_indices, + arguments, + expr, + } = argument_info; // special commands match command_head { // complete module file/directory - // TODO: if module file already specified, + b"use" | b"export use" | b"overlay use" | b"source-env" + if positional_arg_indices.len() == 1 => + { + return self.process_completion( + &mut DotNuCompletion { + std_virtual_path: command_head != b"source-env", + }, + ctx, + ); + } + // NOTE: if module file already specified, // should parse it to get modules/commands/consts to complete - b"use" | b"export use" | b"overlay use" | b"source-env" => { - return self.process_completion(&mut DotNuCompletion, ctx); + b"use" | b"export use" => { + let Some(Argument::Positional(Expression { + expr: Expr::String(module_name), + span, + .. + })) = positional_arg_indices + .first() + .and_then(|i| arguments.get(*i)) + else { + return vec![]; + }; + let module_name = module_name.as_bytes(); + let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) { + Some(module_id) => (module_id, None), + None => { + let mut temp_working_set = + StateWorkingSet::new(ctx.working_set.permanent_state); + let Some(module_id) = parse_module_file_or_dir( + &mut temp_working_set, + module_name, + *span, + None, + ) else { + return vec![]; + }; + (module_id, Some(temp_working_set)) + } + }; + let mut exportable_completion = ExportableCompletion { + module_id, + temp_working_set, + }; + let mut complete_on_list_items = |items: &[ListItem]| -> Vec { + for item in items { + let span = item.expr().span; + if span.contains(pos) { + let offset = span.start.saturating_sub(ctx.span.start); + let end_offset = + ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1); + let new_ctx = Context::new( + ctx.working_set, + Span::new(span.start, ctx.span.end.min(span.end)), + ctx.prefix.get(offset..end_offset).unwrap_or_default(), + ctx.offset, + ); + return self.process_completion(&mut exportable_completion, &new_ctx); + } + } + vec![] + }; + + match &expr.expr { + Expr::String(_) => { + return self.process_completion(&mut exportable_completion, ctx); + } + Expr::FullCellPath(fcp) => match &fcp.head.expr { + Expr::List(items) => { + return complete_on_list_items(items); + } + _ => return vec![], + }, + _ => return vec![], + } } b"which" => { let mut completer = CommandCompletion { diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index e8179e0ec9..9e8e8fe119 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -150,7 +150,7 @@ impl OriginalCwd { } } -fn surround_remove(partial: &str) -> String { +pub fn surround_remove(partial: &str) -> String { for c in ['`', '"', '\''] { if partial.starts_with(c) { let ret = partial.strip_prefix(c).unwrap_or(partial); diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index 084d52d65b..06cf4237fb 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -1,18 +1,23 @@ -use crate::completions::{file_path_completion, Completer, CompletionOptions}; +use crate::completions::{ + completion_common::{surround_remove, FileSuggestion}, + completion_options::NuMatcher, + file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, +}; use nu_path::expand_tilde; use nu_protocol::{ - engine::{Stack, StateWorkingSet}, + engine::{Stack, StateWorkingSet, VirtualPath}, Span, }; use reedline::Suggestion; use std::{ collections::HashSet, - path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}, + path::{is_separator, PathBuf, MAIN_SEPARATOR_STR}, }; -use super::{SemanticSuggestion, SuggestionKind}; - -pub struct DotNuCompletion; +pub struct DotNuCompletion { + /// e.g. use std/a + pub std_virtual_path: bool, +} impl Completer for DotNuCompletion { fn fetch( @@ -102,7 +107,7 @@ impl Completer for DotNuCompletion { // Fetch the files filtering the ones that ends with .nu // and transform them into suggestions - let completions = file_path_completion( + let mut completions = file_path_completion( span, partial, &search_dirs @@ -113,17 +118,60 @@ impl Completer for DotNuCompletion { working_set.permanent_state, stack, ); + + if self.std_virtual_path { + let mut matcher = NuMatcher::new(partial, options); + let base_dir = surround_remove(&base_dir); + if base_dir == "." { + let surround_prefix = partial + .chars() + .take_while(|c| "`'\"".contains(*c)) + .collect::(); + for path in ["std", "std-rfc"] { + let path = format!("{}{}", surround_prefix, path); + matcher.add( + path.clone(), + FileSuggestion { + span, + path, + style: None, + is_dir: true, + }, + ); + } + } else if let Some(VirtualPath::Dir(sub_paths)) = + working_set.find_virtual_path(&base_dir) + { + for sub_vp_id in sub_paths { + let (path, sub_vp) = working_set.get_virtual_path(*sub_vp_id); + let path = path + .strip_prefix(&format!("{}/", base_dir)) + .unwrap_or(path) + .to_string(); + matcher.add( + path.clone(), + FileSuggestion { + path, + span, + style: None, + is_dir: matches!(sub_vp, VirtualPath::Dir(_)), + }, + ); + } + } + completions.extend(matcher.results()); + } + completions .into_iter() // Different base dir, so we list the .nu files or folders .filter(|it| { // for paths with spaces in them let path = it.path.trim_end_matches('`'); - path.ends_with(".nu") || path.ends_with(SEP) + path.ends_with(".nu") || it.is_dir }) .map(|x| { - let append_whitespace = - x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote); + let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote); // Re-calculate the span to replace let mut span_offset = 0; let mut value = x.path.to_string(); diff --git a/crates/nu-cli/src/completions/exportable_completions.rs b/crates/nu-cli/src/completions/exportable_completions.rs new file mode 100644 index 0000000000..a375b2556c --- /dev/null +++ b/crates/nu-cli/src/completions/exportable_completions.rs @@ -0,0 +1,111 @@ +use crate::completions::{ + completion_common::surround_remove, completion_options::NuMatcher, Completer, + CompletionOptions, SemanticSuggestion, SuggestionKind, +}; +use nu_protocol::{ + engine::{Stack, StateWorkingSet}, + ModuleId, Span, +}; +use reedline::Suggestion; + +pub struct ExportableCompletion<'a> { + pub module_id: ModuleId, + pub temp_working_set: Option>, +} + +/// If name contains space, wrap it in quotes +fn wrapped_name(name: String) -> String { + if !name.contains(' ') { + return name; + } + if name.contains('\'') { + format!("\"{}\"", name.replace('"', r#"\""#)) + } else { + format!("'{name}'") + } +} + +impl Completer for ExportableCompletion<'_> { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + _stack: &Stack, + prefix: impl AsRef, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + let mut matcher = NuMatcher::<()>::new(surround_remove(prefix.as_ref()), options); + let mut results = Vec::new(); + let span = reedline::Span { + start: span.start - offset, + end: span.end - offset, + }; + // TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix + let mut add_suggestion = |value: String, + description: Option, + extra: Option>, + kind: SuggestionKind| { + results.push(SemanticSuggestion { + suggestion: Suggestion { + value, + span, + description, + extra, + ..Suggestion::default() + }, + kind: Some(kind), + }); + }; + + let working_set = self.temp_working_set.as_ref().unwrap_or(working_set); + let module = working_set.get_module(self.module_id); + + for (name, decl_id) in &module.decls { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let cmd = working_set.get_decl(*decl_id); + add_suggestion( + wrapped_name(name), + Some(cmd.description().to_string()), + None, + SuggestionKind::Command(cmd.command_type()), + ); + } + } + for (name, module_id) in &module.submodules { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let comments = working_set.get_module_comments(*module_id).map(|spans| { + spans + .iter() + .map(|sp| { + String::from_utf8_lossy(working_set.get_span_contents(*sp)).into() + }) + .collect::>() + }); + add_suggestion( + wrapped_name(name), + Some("Submodule".into()), + comments, + SuggestionKind::Module, + ); + } + } + for (name, var_id) in &module.constants { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let var = working_set.get_variable(*var_id); + add_suggestion( + wrapped_name(name), + var.const_val + .as_ref() + .and_then(|v| v.clone().coerce_into_string().ok()), + None, + SuggestionKind::Variable, + ); + } + } + results + } +} diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index 5c2c542422..387df1b2d9 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -1,12 +1,12 @@ -use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions}; +use crate::completions::{ + completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, +}; use nu_protocol::{ engine::{Stack, StateWorkingSet}, DeclId, Span, }; use reedline::Suggestion; -use super::{SemanticSuggestion, SuggestionKind}; - #[derive(Clone)] pub struct FlagCompletion { pub decl_id: DeclId, diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 5f16338bc2..b67d7db354 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -8,6 +8,7 @@ mod completion_options; mod custom_completions; mod directory_completions; mod dotnu_completions; +mod exportable_completions; mod file_completions; mod flag_completions; mod operator_completions; @@ -22,6 +23,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm}; pub use custom_completions::CustomCompletion; pub use directory_completions::DirectoryCompletion; pub use dotnu_completions::DotNuCompletion; +pub use exportable_completions::ExportableCompletion; pub use file_completions::{file_path_completion, FileCompletion}; pub use flag_completions::FlagCompletion; pub use operator_completions::OperatorCompletion; diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index e2b481f676..7559387999 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -11,6 +11,7 @@ use nu_engine::eval_block; use nu_parser::parse; use nu_path::expand_tilde; use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData}; +use nu_std::load_standard_library; use reedline::{Completer, Suggestion}; use rstest::{fixture, rstest}; use support::{ @@ -513,7 +514,7 @@ fn dotnu_completions() { match_suggestions(&vec!["sub.nu`"], &suggestions); - let expected = vec![ + let mut expected = vec![ "asdf.nu", "bar.nu", "bat.nu", @@ -546,6 +547,8 @@ fn dotnu_completions() { match_suggestions(&expected, &suggestions); // Test use completion + expected.push("std"); + expected.push("std-rfc"); let completion_str = "use "; let suggestions = completer.complete(completion_str, completion_str.len()); @@ -577,6 +580,65 @@ fn dotnu_completions() { match_dir_content_for_dotnu(dir_content, &suggestions); } +#[test] +fn dotnu_stdlib_completions() { + let (_, _, mut engine, stack) = new_dotnu_engine(); + assert!(load_standard_library(&mut engine).is_ok()); + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let completion_str = "export use std/ass"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["assert"], &suggestions); + + let completion_str = "use `std-rfc/cli"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["clip"], &suggestions); + + let completion_str = "use \"std"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["\"std", "\"std-rfc"], &suggestions); + + let completion_str = "overlay use \'std-rfc/cli"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["clip"], &suggestions); +} + +#[test] +fn exportable_completions() { + let (_, _, mut engine, mut stack) = new_dotnu_engine(); + let code = r#"export module "๐Ÿค”๐Ÿ˜" { + export const foo = "๐Ÿค”๐Ÿ˜"; + }"#; + assert!(support::merge_input(code.as_bytes(), &mut engine, &mut stack).is_ok()); + assert!(load_standard_library(&mut engine).is_ok()); + + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let completion_str = "use std null"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["null-device", "null_device"], &suggestions); + + let completion_str = "export use std/assert eq"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["equal"], &suggestions); + + let completion_str = "use std/assert \"not eq"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["'not equal'"], &suggestions); + + let completion_str = "use std-rfc/clip ['prefi"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["prefix"], &suggestions); + + let completion_str = "use std/math [E, `TAU"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["TAU"], &suggestions); + + let completion_str = "use ๐Ÿค”๐Ÿ˜ 'foo"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["foo"], &suggestions); +} + #[test] fn dotnu_completions_const_nu_lib_dirs() { let (_, _, engine, stack) = new_dotnu_engine(); diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index 5f3fabbf19..fbc444b418 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -28,6 +28,7 @@ url = { workspace = true } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" } nu-command = { path = "../nu-command", version = "0.103.1" } nu-engine = { path = "../nu-engine", version = "0.103.1" } +nu-std = { path = "../nu-std", version = "0.103.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.1" } assert-json-diff = "2.0" diff --git a/crates/nu-lsp/src/completion.rs b/crates/nu-lsp/src/completion.rs index 16ae33567f..110f5d4f0f 100644 --- a/crates/nu-lsp/src/completion.rs +++ b/crates/nu-lsp/src/completion.rs @@ -28,22 +28,15 @@ impl LanguageServer { .and_then(|s| s.chars().next()) .is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c)); - let (results, engine_state) = if need_fallback { - let engine_state = Arc::new(self.initial_engine_state.clone()); - let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); - ( - completer.fetch_completions_at(&file_text[..location], location), - engine_state, - ) + self.need_parse |= need_fallback; + let engine_state = Arc::new(self.new_engine_state()); + let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); + let results = if need_fallback { + completer.fetch_completions_at(&file_text[..location], location) } else { - let engine_state = Arc::new(self.new_engine_state()); - let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); let file_path = uri_to_path(&path_uri); let filename = file_path.to_str()?; - ( - completer.fetch_completions_within_file(filename, location, &file_text), - engine_state, - ) + completer.fetch_completions_within_file(filename, location, &file_text) }; let docs = self.docs.lock().ok()?; @@ -63,10 +56,8 @@ impl LanguageServer { } let span = r.suggestion.span; - let range = span_to_range(&Span::new(span.start, span.end), file, 0); - let text_edit = Some(CompletionTextEdit::Edit(TextEdit { - range, + range: span_to_range(&Span::new(span.start, span.end), file, 0), new_text: label_value.clone(), })); @@ -236,7 +227,7 @@ mod tests { "detail": "Edit nu configurations.", "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, }, "newText": "config nu " - } + }, }, ]) ); @@ -549,4 +540,96 @@ mod tests { ]) ); } + + #[test] + fn complete_use_arguments() { + let (client_connection, _recv) = initialize_language_server(None, None); + + let mut script = fixtures(); + script.push("lsp"); + script.push("completion"); + script.push("use.nu"); + let script = path_to_uri(&script); + + open_unchecked(&client_connection, script.clone()); + let resp = send_complete_request(&client_connection, script.clone(), 4, 17); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "std-rfc", + "labelDetails": { "description": "module" }, + "textEdit": { + "newText": "std-rfc", + "range": { "start": { "character": 11, "line": 4 }, "end": { "character": 17, "line": 4 } } + }, + "kind": 9 // module kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 5, 22); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "clip", + "labelDetails": { "description": "module" }, + "textEdit": { + "newText": "clip", + "range": { "start": { "character": 19, "line": 5 }, "end": { "character": 23, "line": 5 } } + }, + "kind": 9 // module kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 5, 35); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "paste", + "labelDetails": { "description": "custom" }, + "textEdit": { + "newText": "paste", + "range": { "start": { "character": 32, "line": 5 }, "end": { "character": 37, "line": 5 } } + }, + "kind": 2 + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 6, 14); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "null_device", + "labelDetails": { "description": "variable" }, + "textEdit": { + "newText": "null_device", + "range": { "start": { "character": 8, "line": 6 }, "end": { "character": 14, "line": 6 } } + }, + "kind": 6 // variable kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script, 7, 13); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "foo", + "labelDetails": { "description": "variable" }, + "textEdit": { + "newText": "foo", + "range": { "start": { "character": 11, "line": 7 }, "end": { "character": 14, "line": 7 } } + }, + "kind": 6 // variable kind + } + ]) + ); + } } diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 027c1ffe8c..d48e425c43 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -440,6 +440,7 @@ mod tests { TextDocumentPositionParams, WorkDoneProgressParams, }; use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value}; + use nu_std::load_standard_library; use std::sync::mpsc::{self, Receiver}; use std::time::Duration; @@ -455,6 +456,7 @@ mod tests { let engine_state = nu_cmd_lang::create_default_context(); let mut engine_state = nu_command::add_shell_command_context(engine_state); engine_state.generate_nu_constant(); + assert!(load_standard_library(&mut engine_state).is_ok()); let cwd = std::env::current_dir().expect("Could not get current working directory."); engine_state.add_env_var( "PWD".into(), diff --git a/tests/fixtures/lsp/completion/use.nu b/tests/fixtures/lsp/completion/use.nu new file mode 100644 index 0000000000..8439560fb5 --- /dev/null +++ b/tests/fixtures/lsp/completion/use.nu @@ -0,0 +1,8 @@ +export module "๐Ÿค”๐Ÿ˜" { + export const foo = "๐Ÿค”๐Ÿ˜"; +} + +export use std-rf +export use std-rfc/clip [ copy, paste ] +use std null_d +use ๐Ÿค”๐Ÿ˜ [ foo, ] From f39e5b3f37be6538fc8da174bc294c5640e9676c Mon Sep 17 00:00:00 2001 From: Wind Date: Tue, 1 Apr 2025 20:15:39 +0800 Subject: [PATCH 05/17] Update rand and rand_chacha to 0.9 (#15463) # Description As description, I think it's worth to move forward to update rand and rand_chacha to 0.9. # User-Facing Changes Hopefully none # Tests + Formatting NaN # After Submitting NaN --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- crates/nu-command/src/filters/shuffle.rs | 4 ++-- crates/nu-command/src/random/bool.rs | 6 ++---- crates/nu-command/src/random/byte_stream.rs | 8 +++---- crates/nu-command/src/random/dice.rs | 7 ++----- crates/nu-command/src/random/float.rs | 12 +++++------ crates/nu-command/src/random/int.rs | 12 +++++------ crates/nu-command/tests/commands/base/mod.rs | 3 ++- .../tests/commands/database/into_sqlite.rs | 21 ++++++++++--------- 10 files changed, 37 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5bdaf61196..7e0f5c0f90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3702,8 +3702,8 @@ dependencies = [ "print-positions", "procfs", "quick-xml 0.37.1", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.0", + "rand_chacha 0.9.0", "rayon", "rmp", "roxmltree", diff --git a/Cargo.toml b/Cargo.toml index 3d11084007..0200139699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,9 +135,9 @@ quick-xml = "0.37.0" quickcheck = "1.0" quickcheck_macros = "1.0" quote = "1.0" -rand = "0.8" +rand = "0.9" getrandom = "0.2" # pick same version that rand requires -rand_chacha = "0.3.1" +rand_chacha = "0.9" ratatui = "0.29" rayon = "1.10" reedline = "0.39.0" diff --git a/crates/nu-command/src/filters/shuffle.rs b/crates/nu-command/src/filters/shuffle.rs index 35e64f8ce7..0193f2e2f3 100644 --- a/crates/nu-command/src/filters/shuffle.rs +++ b/crates/nu-command/src/filters/shuffle.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use rand::{prelude::SliceRandom, thread_rng}; +use rand::{prelude::SliceRandom, rng}; #[derive(Clone)] pub struct Shuffle; @@ -31,7 +31,7 @@ impl Command for Shuffle { ) -> Result { let metadata = input.metadata(); let mut values = input.into_iter_strict(call.head)?.collect::>(); - values.shuffle(&mut thread_rng()); + values.shuffle(&mut rng()); let iter = values.into_iter(); Ok(iter.into_pipeline_data_with_metadata( call.head, diff --git a/crates/nu-command/src/random/bool.rs b/crates/nu-command/src/random/bool.rs index 51870a7836..f2f07c8c5a 100644 --- a/crates/nu-command/src/random/bool.rs +++ b/crates/nu-command/src/random/bool.rs @@ -1,6 +1,5 @@ use nu_engine::command_prelude::*; - -use rand::prelude::{thread_rng, Rng}; +use rand::random_bool; #[derive(Clone)] pub struct RandomBool; @@ -77,8 +76,7 @@ fn bool( } } - let mut rng = thread_rng(); - let bool_result: bool = rng.gen_bool(probability); + let bool_result: bool = random_bool(probability); Ok(PipelineData::Value(Value::bool(bool_result, span), None)) } diff --git a/crates/nu-command/src/random/byte_stream.rs b/crates/nu-command/src/random/byte_stream.rs index baff8fbc6c..26d2482e6a 100644 --- a/crates/nu-command/src/random/byte_stream.rs +++ b/crates/nu-command/src/random/byte_stream.rs @@ -1,8 +1,8 @@ use nu_engine::command_prelude::*; use nu_protocol::Signals; use rand::{ - distributions::{Alphanumeric, Standard}, - thread_rng, Rng, + distr::{Alphanumeric, StandardUniform}, + rng, Rng, }; pub(super) enum RandomDistribution { @@ -31,9 +31,9 @@ pub(super) fn random_byte_stream( let bytes_to_write = std::cmp::min(remaining_bytes, OUTPUT_CHUNK_SIZE); - let rng = thread_rng(); + let rng = rng(); let byte_iter: Box> = match distribution { - RandomDistribution::Binary => Box::new(rng.sample_iter(Standard)), + RandomDistribution::Binary => Box::new(rng.sample_iter(StandardUniform)), RandomDistribution::Alphanumeric => Box::new(rng.sample_iter(Alphanumeric)), }; out.extend(byte_iter.take(bytes_to_write)); diff --git a/crates/nu-command/src/random/dice.rs b/crates/nu-command/src/random/dice.rs index c9720e49bb..d195e10f6f 100644 --- a/crates/nu-command/src/random/dice.rs +++ b/crates/nu-command/src/random/dice.rs @@ -1,6 +1,6 @@ use nu_engine::command_prelude::*; use nu_protocol::ListStream; -use rand::prelude::{thread_rng, Rng}; +use rand::random_range; #[derive(Clone)] pub struct RandomDice; @@ -73,10 +73,7 @@ fn dice( let dice: usize = call.get_flag(engine_state, stack, "dice")?.unwrap_or(1); let sides: usize = call.get_flag(engine_state, stack, "sides")?.unwrap_or(6); - let iter = (0..dice).map(move |_| { - let mut thread_rng = thread_rng(); - Value::int(thread_rng.gen_range(1..sides + 1) as i64, span) - }); + let iter = (0..dice).map(move |_| Value::int(random_range(1..sides + 1) as i64, span)); Ok(ListStream::new(iter, span, engine_state.signals().clone()).into()) } diff --git a/crates/nu-command/src/random/float.rs b/crates/nu-command/src/random/float.rs index fb7cc329b4..f47d128b2c 100644 --- a/crates/nu-command/src/random/float.rs +++ b/crates/nu-command/src/random/float.rs @@ -1,6 +1,6 @@ use nu_engine::command_prelude::*; use nu_protocol::{FloatRange, Range}; -use rand::prelude::{thread_rng, Rng}; +use rand::random_range; use std::ops::Bound; #[derive(Clone)] @@ -71,8 +71,6 @@ fn float( let span = call.head; let range: Option> = call.opt(engine_state, stack, 0)?; - let mut thread_rng = thread_rng(); - match range { Some(range) => { let range_span = range.span; @@ -90,15 +88,15 @@ fn float( } let value = match range.end() { - Bound::Included(end) => thread_rng.gen_range(range.start()..=end), - Bound::Excluded(end) => thread_rng.gen_range(range.start()..end), - Bound::Unbounded => thread_rng.gen_range(range.start()..f64::INFINITY), + Bound::Included(end) => random_range(range.start()..=end), + Bound::Excluded(end) => random_range(range.start()..end), + Bound::Unbounded => random_range(range.start()..f64::INFINITY), }; Ok(PipelineData::Value(Value::float(value, span), None)) } None => Ok(PipelineData::Value( - Value::float(thread_rng.gen_range(0.0..1.0), span), + Value::float(random_range(0.0..1.0), span), None, )), } diff --git a/crates/nu-command/src/random/int.rs b/crates/nu-command/src/random/int.rs index 2adff1ec70..90e3a0fbaf 100644 --- a/crates/nu-command/src/random/int.rs +++ b/crates/nu-command/src/random/int.rs @@ -1,6 +1,6 @@ use nu_engine::command_prelude::*; use nu_protocol::Range; -use rand::prelude::{thread_rng, Rng}; +use rand::random_range; use std::ops::Bound; #[derive(Clone)] @@ -75,8 +75,6 @@ fn integer( let span = call.head; let range: Option> = call.opt(engine_state, stack, 0)?; - let mut thread_rng = thread_rng(); - match range { Some(range) => { let range_span = range.span; @@ -94,9 +92,9 @@ fn integer( } let value = match range.end() { - Bound::Included(end) => thread_rng.gen_range(range.start()..=end), - Bound::Excluded(end) => thread_rng.gen_range(range.start()..end), - Bound::Unbounded => thread_rng.gen_range(range.start()..=i64::MAX), + Bound::Included(end) => random_range(range.start()..=end), + Bound::Excluded(end) => random_range(range.start()..end), + Bound::Unbounded => random_range(range.start()..=i64::MAX), }; Ok(PipelineData::Value(Value::int(value, span), None)) @@ -110,7 +108,7 @@ fn integer( } } None => Ok(PipelineData::Value( - Value::int(thread_rng.gen_range(0..=i64::MAX), span), + Value::int(random_range(0..=i64::MAX), span), None, )), } diff --git a/crates/nu-command/tests/commands/base/mod.rs b/crates/nu-command/tests/commands/base/mod.rs index 308f0b35df..7fef836350 100644 --- a/crates/nu-command/tests/commands/base/mod.rs +++ b/crates/nu-command/tests/commands/base/mod.rs @@ -1,5 +1,6 @@ use data_encoding::HEXUPPER; use rand::prelude::*; +use rand::random_range; use rand_chacha::ChaCha8Rng; use nu_test_support::nu; @@ -16,7 +17,7 @@ fn random_bytes() -> Vec { (0..NUM) .map(|_| { - let length = rng.gen_range(0..512); + let length = random_range(0..512); let mut bytes = vec![0u8; length]; rng.fill_bytes(&mut bytes); HEXUPPER.encode(&bytes) diff --git a/crates/nu-command/tests/commands/database/into_sqlite.rs b/crates/nu-command/tests/commands/database/into_sqlite.rs index 856757e768..93be9f27db 100644 --- a/crates/nu-command/tests/commands/database/into_sqlite.rs +++ b/crates/nu-command/tests/commands/database/into_sqlite.rs @@ -7,8 +7,9 @@ use nu_test_support::{ playground::{Dirs, Playground}, }; use rand::{ - distributions::{Alphanumeric, DistString, Standard}, + distr::{Alphanumeric, SampleString, StandardUniform}, prelude::Distribution, + random_range, rngs::StdRng, Rng, SeedableRng, }; @@ -382,7 +383,7 @@ struct TestRow( impl TestRow { pub fn random() -> Self { - StdRng::from_entropy().sample(Standard) + StdRng::from_os_rng().sample(StandardUniform) } } @@ -433,12 +434,12 @@ impl TryFrom<&rusqlite::Row<'_>> for TestRow { } } -impl Distribution for Standard { +impl Distribution for StandardUniform { fn sample(&self, rng: &mut R) -> TestRow where R: rand::Rng + ?Sized, { - let dt = DateTime::from_timestamp_millis(rng.gen_range(0..2324252554000)) + let dt = DateTime::from_timestamp_millis(random_range(0..2324252554000)) .unwrap() .fixed_offset(); @@ -446,18 +447,18 @@ impl Distribution for Standard { // limit the size of the numbers to work around // https://github.com/nushell/nushell/issues/10612 - let filesize = rng.gen_range(-1024..=1024); - let duration = rng.gen_range(-1024..=1024); + let filesize = random_range(-1024..=1024); + let duration = random_range(-1024..=1024); TestRow( - rng.gen(), - rng.gen(), - rng.gen(), + rng.random(), + rng.random(), + rng.random(), filesize, duration, dt, rand_string, - rng.gen::().to_be_bytes().to_vec(), + rng.random::().to_be_bytes().to_vec(), rusqlite::types::Value::Null, ) } From 43f9ec295f7ed57dfb488612ea5810f9dbce6496 Mon Sep 17 00:00:00 2001 From: Wind Date: Tue, 1 Apr 2025 20:17:05 +0800 Subject: [PATCH 06/17] remove -s, -p in do (#15456) # Description Closes #15450 # User-Facing Changes do can't use `-s`, `-p` after this pr # Tests + Formatting Removed 3 tests. # After Submitting NaN --- crates/nu-cmd-lang/src/core_commands/do_.rs | 46 ++------------------- crates/nu-command/tests/commands/do_.rs | 24 ----------- 2 files changed, 3 insertions(+), 67 deletions(-) diff --git a/crates/nu-cmd-lang/src/core_commands/do_.rs b/crates/nu-cmd-lang/src/core_commands/do_.rs index fe788983da..db0617db3b 100644 --- a/crates/nu-cmd-lang/src/core_commands/do_.rs +++ b/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -31,16 +31,6 @@ impl Command for Do { "ignore errors as the closure runs", Some('i'), ) - .switch( - "ignore-shell-errors", - "ignore shell errors as the closure runs", - Some('s'), - ) - .switch( - "ignore-program-errors", - "ignore external program errors as the closure runs", - Some('p'), - ) .switch( "capture-errors", "catch errors as the closure runs, and return them", @@ -71,36 +61,6 @@ impl Command for Do { let rest: Vec = call.rest(engine_state, caller_stack, 1)?; let ignore_all_errors = call.has_flag(engine_state, caller_stack, "ignore-errors")?; - if call.has_flag(engine_state, caller_stack, "ignore-shell-errors")? { - nu_protocol::report_shell_warning( - engine_state, - &ShellError::GenericError { - error: "Deprecated option".into(), - msg: "`--ignore-shell-errors` is deprecated and will be removed in 0.102.0." - .into(), - span: Some(call.head), - help: Some("Please use the `--ignore-errors(-i)`".into()), - inner: vec![], - }, - ); - } - if call.has_flag(engine_state, caller_stack, "ignore-program-errors")? { - nu_protocol::report_shell_warning( - engine_state, - &ShellError::GenericError { - error: "Deprecated option".into(), - msg: "`--ignore-program-errors` is deprecated and will be removed in 0.102.0." - .into(), - span: Some(call.head), - help: Some("Please use the `--ignore-errors(-i)`".into()), - inner: vec![], - }, - ); - } - let ignore_shell_errors = ignore_all_errors - || call.has_flag(engine_state, caller_stack, "ignore-shell-errors")?; - let ignore_program_errors = ignore_all_errors - || call.has_flag(engine_state, caller_stack, "ignore-program-errors")?; let capture_errors = call.has_flag(engine_state, caller_stack, "capture-errors")?; let has_env = call.has_flag(engine_state, caller_stack, "env")?; @@ -206,7 +166,7 @@ impl Command for Do { } } Ok(PipelineData::ByteStream(mut stream, metadata)) - if ignore_program_errors + if ignore_all_errors && !matches!( caller_stack.stdout(), OutDest::Pipe | OutDest::PipeSeparate | OutDest::Value @@ -218,10 +178,10 @@ impl Command for Do { } Ok(PipelineData::ByteStream(stream, metadata)) } - Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_shell_errors => { + Ok(PipelineData::Value(Value::Error { .. }, ..)) | Err(_) if ignore_all_errors => { Ok(PipelineData::empty()) } - Ok(PipelineData::ListStream(stream, metadata)) if ignore_shell_errors => { + Ok(PipelineData::ListStream(stream, metadata)) if ignore_all_errors => { let stream = stream.map(move |value| { if let Value::Error { .. } = value { Value::nothing(head) diff --git a/crates/nu-command/tests/commands/do_.rs b/crates/nu-command/tests/commands/do_.rs index 1a2258cf9c..606c534073 100644 --- a/crates/nu-command/tests/commands/do_.rs +++ b/crates/nu-command/tests/commands/do_.rs @@ -40,22 +40,6 @@ fn do_with_semicolon_break_on_failed_external() { assert_eq!(actual.out, ""); } -#[test] -fn ignore_shell_errors_works_for_external_with_semicolon() { - let actual = nu!(r#"do -s { open asdfasdf.txt }; "text""#); - - assert!(actual.err.contains("Deprecated option")); - assert_eq!(actual.out, "text"); -} - -#[test] -fn ignore_program_errors_works_for_external_with_semicolon() { - let actual = nu!(r#"do -p { nu -n -c 'exit 1' }; "text""#); - - assert!(actual.err.contains("Deprecated option")); - assert_eq!(actual.out, "text"); -} - #[test] fn ignore_error_should_work_for_external_command() { let actual = nu!(r#"do -i { nu --testbin fail asdf }; echo post"#); @@ -76,11 +60,3 @@ fn run_closure_with_it_using() { assert!(actual.err.is_empty()); assert_eq!(actual.out, "3"); } - -#[test] -fn waits_for_external() { - let actual = nu!(r#"do -p { nu -c 'sleep 1sec; print before; exit 1'}; print after"#); - - assert!(actual.err.contains("Deprecated option")); - assert_eq!(actual.out, "beforeafter"); -} From 9ba16dbdaf50db1fdbab0ec0976f1d82226343b4 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Tue, 1 Apr 2025 08:17:36 -0400 Subject: [PATCH 07/17] Add boolean examples to `any` and `all` (#15442) # Description Follow-up to #15277 and #15392. Adds examples to `any` and `all` demonstrating using `any {}` or `all {}` with lists of booleans. We have a couple options that work for this use-case, but not sure which we should recommend. The PR currently uses (1). 1. `any {}` / `all {}` 2. `any { $in }` / `all { $in }` 3. `any { $in == true }` / `all { $in == true }` Would love to hear your thoughts on the above @fennewald @mtimaN @fdncred @NotTheDr01ds @ysthakur # User-Facing Changes * Added an extra example for `any` and `all` # Tests + Formatting N/A # After Submitting N/A --- crates/nu-command/src/filters/all.rs | 5 +++++ crates/nu-command/src/filters/any.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/crates/nu-command/src/filters/all.rs b/crates/nu-command/src/filters/all.rs index aa36118273..844d845a54 100644 --- a/crates/nu-command/src/filters/all.rs +++ b/crates/nu-command/src/filters/all.rs @@ -30,6 +30,11 @@ impl Command for All { fn examples(&self) -> Vec { vec![ + Example { + description: "Check if a list contains only true values", + example: "[false true true false] | all {}", + result: Some(Value::test_bool(false)), + }, Example { description: "Check if each row's status is the string 'UP'", example: "[[status]; [UP] [UP]] | all {|el| $el.status == UP }", diff --git a/crates/nu-command/src/filters/any.rs b/crates/nu-command/src/filters/any.rs index 230a96ebe7..3d6f5404a2 100644 --- a/crates/nu-command/src/filters/any.rs +++ b/crates/nu-command/src/filters/any.rs @@ -30,6 +30,11 @@ impl Command for Any { fn examples(&self) -> Vec { vec![ + Example { + description: "Check if a list contains any true values", + example: "[false true true false] | any {}", + result: Some(Value::test_bool(true)), + }, Example { description: "Check if any row's status is the string 'DOWN'", example: "[[status]; [UP] [DOWN] [UP]] | any {|el| $el.status == DOWN }", From a23e96c94568078c645b7635192f9715f3d9853e Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:18:11 -0500 Subject: [PATCH 08/17] update human-date-parser to 3.0 (#15426) # Description There's been much debate about whether to keep human-date-parser in `into datetime`. We saw recently that a new version of the crate was released that addressed some of our concerns. This PR is to make it easier to test those fixes. # User-Facing Changes # Tests + Formatting # After Submitting --- Cargo.lock | 27 +++++++++++++++++-- Cargo.toml | 2 +- .../src/conversions/into/datetime.rs | 26 ++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e0f5c0f90..abd0032bb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2386,12 +2386,13 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human-date-parser" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1116cf4debfe770c12168458321c4a8591b71c4c19f7100de07c84cf81701c63" +checksum = "406f83c56de4b2c9183be52ae9a4fefa22c0e0c3d3d7ef80be26eaee11c7110e" dependencies = [ "chrono", "pest", + "pest_consume", "pest_derive", "thiserror 1.0.69", ] @@ -4677,6 +4678,28 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pest_consume" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79447402d15d18e7142e14c72f2e63fa3d155be1bc5b70b3ccbb610ac55f536b" +dependencies = [ + "pest", + "pest_consume_macros", + "pest_derive", +] + +[[package]] +name = "pest_consume_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8630a7a899cb344ec1c16ba0a6b24240029af34bdc0a21f84e411d7f793f29" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pest_derive" version = "2.7.15" diff --git a/Cargo.toml b/Cargo.toml index 0200139699..c2a4bf7e37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ fancy-regex = "0.14" filesize = "0.2" filetime = "0.2" heck = "0.5.0" -human-date-parser = "0.2.0" +human-date-parser = "0.3.0" indexmap = "2.8" indicatif = "0.17" interprocess = "2.2.0" diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index eaac1395e1..1ff2d55522 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -294,7 +294,7 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { match parse_date_from_string(&input_val, span) { Ok(date) => return Value::date(date, span), Err(_) => { - if let Ok(date) = from_human_time(&input_val) { + if let Ok(date) = from_human_time(&input_val, Local::now().naive_local()) { match date { ParseResult::Date(date) => { let time = Local::now().time(); @@ -307,7 +307,29 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { return Value::date(dt_fixed, span); } ParseResult::DateTime(date) => { - return Value::date(date.fixed_offset(), span) + let local_offset = *Local::now().offset(); + let dt_fixed = match local_offset.from_local_datetime(&date) { + chrono::LocalResult::Single(dt) => dt, + chrono::LocalResult::Ambiguous(_, _) => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Ambiguous datetime".to_string(), + span, + }, + span, + ); + } + chrono::LocalResult::None => { + return Value::error( + ShellError::DatetimeParseError { + msg: "Invalid datetime".to_string(), + span, + }, + span, + ); + } + }; + return Value::date(dt_fixed, span); } ParseResult::Time(time) => { let date = Local::now().date_naive(); From 470d130289315c46f0bef21541b9d39803a0b1a7 Mon Sep 17 00:00:00 2001 From: pyz4 <42039243+pyz4@users.noreply.github.com> Date: Tue, 1 Apr 2025 19:22:05 -0400 Subject: [PATCH 09/17] `polars cast`: add decimal option for dtype parameter (#15464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR expands the `dtype` parameter of the `polars cast` command to include `decimal` type. Setting precision to "*" will compel inferring the value. Note, however, setting scale to a non-integer value will throw an explicit error (the underlying polars crate assigns scale = 0 in such a case, but I opted for throwing an error instead). . ``` $ [[a b]; [1 2] [3 4]] | polars into-df | polars cast decimal<4,2> a | polars schema โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ a โ”‚ decimal<4,2> โ”‚ โ”‚ b โ”‚ i64 โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ $ [[a b]; [10.5 2] [3.1 4]] | polars into-df | polars cast decimal<*,2> a | polars schema โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ a โ”‚ decimal<*,2> โ”‚ โ”‚ b โ”‚ i64 โ”‚ โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ $ [[a b]; [10.05 2] [3.1 4]] | polars into-df | polars cast decimal<5,*> a | polars schema rror: ร— Invalid polars data type โ•ญโ”€[entry #25:1:47] 1 โ”‚ [[a b]; [10.05 2] [3.1 4]] | polars into-df | polars cast decimal<5,*> a | polars schema ยท โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€ ยท โ•ฐโ”€โ”€ `*` is not a permitted value for scale โ•ฐโ”€โ”€โ”€โ”€ ``` # User-Facing Changes There are no breaking changes. The user has the additional option to `polars cast` to a decimal type # Tests + Formatting Tests have been added to `nu_plugin_polars/src/dataframe/values/nu_schema.rs` --- .../src/dataframe/values/nu_schema.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_schema.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_schema.rs index cf425eb046..1e2ae4723a 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_schema.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_schema.rs @@ -169,6 +169,67 @@ pub fn str_to_dtype(dtype: &str, span: Span) -> Result { let time_unit = str_to_time_unit(next, span)?; Ok(DataType::Duration(time_unit)) } + _ if dtype.starts_with("decimal") => { + let dtype = dtype + .trim_start_matches("decimal") + .trim_start_matches('<') + .trim_end_matches('>'); + let mut split = dtype.split(','); + let next = split + .next() + .ok_or_else(|| ShellError::GenericError { + error: "Invalid polars data type".into(), + msg: "Missing decimal precision".into(), + span: Some(span), + help: None, + inner: vec![], + })? + .trim(); + let precision = match next { + "*" => None, // infer + _ => Some( + next.parse::() + .map_err(|e| ShellError::GenericError { + error: "Invalid polars data type".into(), + msg: format!("Error in parsing decimal precision: {e}"), + span: Some(span), + help: None, + inner: vec![], + })?, + ), + }; + + let next = split + .next() + .ok_or_else(|| ShellError::GenericError { + error: "Invalid polars data type".into(), + msg: "Missing decimal scale".into(), + span: Some(span), + help: None, + inner: vec![], + })? + .trim(); + let scale = match next { + "*" => Err(ShellError::GenericError { + error: "Invalid polars data type".into(), + msg: "`*` is not a permitted value for scale".into(), + span: Some(span), + help: None, + inner: vec![], + }), + _ => next + .parse::() + .map(Some) + .map_err(|e| ShellError::GenericError { + error: "Invalid polars data type".into(), + msg: format!("Error in parsing decimal precision: {e}"), + span: Some(span), + help: None, + inner: vec![], + }), + }?; + Ok(DataType::Decimal(precision, scale)) + } _ => Err(ShellError::GenericError { error: "Invalid polars data type".into(), msg: format!("Unknown type: {dtype}"), @@ -367,6 +428,24 @@ mod test { assert_eq!(schema, expected); } + #[test] + fn test_dtype_str_schema_decimal() { + let dtype = "decimal<7,2>"; + let schema = str_to_dtype(dtype, Span::unknown()).unwrap(); + let expected = DataType::Decimal(Some(7usize), Some(2usize)); + assert_eq!(schema, expected); + + // "*" is not a permitted value for scale + let dtype = "decimal<7,*>"; + let schema = str_to_dtype(dtype, Span::unknown()); + assert!(matches!(schema, Err(ShellError::GenericError { .. }))); + + let dtype = "decimal<*,2>"; + let schema = str_to_dtype(dtype, Span::unknown()).unwrap(); + let expected = DataType::Decimal(None, Some(2usize)); + assert_eq!(schema, expected); + } + #[test] fn test_dtype_str_to_schema_list_types() { let dtype = "list"; @@ -383,5 +462,19 @@ mod test { let schema = str_to_dtype(dtype, Span::unknown()).unwrap(); let expected = DataType::List(Box::new(DataType::Datetime(TimeUnit::Milliseconds, None))); assert_eq!(schema, expected); + + let dtype = "list>"; + let schema = str_to_dtype(dtype, Span::unknown()).unwrap(); + let expected = DataType::List(Box::new(DataType::Decimal(Some(7usize), Some(2usize)))); + assert_eq!(schema, expected); + + let dtype = "list>"; + let schema = str_to_dtype(dtype, Span::unknown()).unwrap(); + let expected = DataType::List(Box::new(DataType::Decimal(None, Some(2usize)))); + assert_eq!(schema, expected); + + let dtype = "list>"; + let schema = str_to_dtype(dtype, Span::unknown()); + assert!(matches!(schema, Err(ShellError::GenericError { .. }))); } } From d7f26b177a6df7ea5dc99e7e09206593daff80e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 08:59:12 +0800 Subject: [PATCH 10/17] build(deps): bump crate-ci/typos from 1.31.0 to 1.31.1 (#15469) --- .github/workflows/typos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 0f4437e2a0..eb57d8c471 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -10,4 +10,4 @@ jobs: uses: actions/checkout@v4.1.7 - name: Check spelling - uses: crate-ci/typos@v1.31.0 + uses: crate-ci/typos@v1.31.1 From af6c4bdc9c291db2af6dedff4ba468d1e8920bc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:18:43 +0800 Subject: [PATCH 11/17] build(deps): bump bytesize from 1.3.2 to 1.3.3 (#15468) Bumps [bytesize](https://github.com/bytesize-rs/bytesize) from 1.3.2 to 1.3.3.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bytesize&package-manager=cargo&previous-version=1.3.2&new-version=1.3.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abd0032bb3..b8be82f5c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,9 +856,9 @@ dependencies = [ [[package]] name = "bytesize" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" [[package]] name = "calamine" diff --git a/Cargo.toml b/Cargo.toml index c2a4bf7e37..11762c72a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ bracoxide = "0.1.5" brotli = "7.0" byteorder = "1.5" bytes = "1" -bytesize = "1.3.1" +bytesize = "1.3.3" calamine = "0.26.1" chardetng = "0.1.17" chrono = { default-features = false, version = "0.4.34" } From df74a0c961d7d05cdfd525b6d632a1c6233a622b Mon Sep 17 00:00:00 2001 From: zc he Date: Wed, 2 Apr 2025 19:12:38 +0800 Subject: [PATCH 12/17] refactor: command identified by name instead of span content (#15471) This should be a more robust method. # Description Previously, `export use` with double-space in between will fail to be recognized as command `export use`. # User-Facing Changes minor bug fix # Tests + Formatting test cases made harder # After Submitting --- crates/nu-cli/src/completions/completer.rs | 12 ++++++------ crates/nu-cli/tests/completions/mod.rs | 3 ++- crates/nu-lsp/src/ast.rs | 16 ++++++++-------- crates/nu-lsp/src/workspace.rs | 14 +++++++------- tests/fixtures/lsp/workspace/foo.nu | 2 +- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index bb56963942..0e010054bc 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -134,7 +134,7 @@ struct Context<'a> { /// For argument completion struct PositionalArguments<'a> { /// command name - command_head: &'a [u8], + command_head: &'a str, /// indices of positional arguments positional_arg_indices: Vec, /// argument list @@ -395,7 +395,7 @@ impl NuCompleter { Argument::Positional(_) if prefix == b"-" => flag_completion_helper(), // complete according to expression type and command head Argument::Positional(expr) => { - let command_head = working_set.get_span_contents(call.head); + let command_head = working_set.get_decl(call.decl_id).name(); positional_arg_indices.push(arg_idx); self.argument_completion_helper( PositionalArguments { @@ -537,19 +537,19 @@ impl NuCompleter { // special commands match command_head { // complete module file/directory - b"use" | b"export use" | b"overlay use" | b"source-env" + "use" | "export use" | "overlay use" | "source-env" if positional_arg_indices.len() == 1 => { return self.process_completion( &mut DotNuCompletion { - std_virtual_path: command_head != b"source-env", + std_virtual_path: command_head != "source-env", }, ctx, ); } // NOTE: if module file already specified, // should parse it to get modules/commands/consts to complete - b"use" | b"export use" => { + "use" | "export use" => { let Some(Argument::Positional(Expression { expr: Expr::String(module_name), span, @@ -613,7 +613,7 @@ impl NuCompleter { _ => return vec![], } } - b"which" => { + "which" => { let mut completer = CommandCompletion { internals: true, externals: true, diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 7559387999..9792f2b19b 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -586,7 +586,8 @@ fn dotnu_stdlib_completions() { assert!(load_standard_library(&mut engine).is_ok()); let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); - let completion_str = "export use std/ass"; + // `export use` should be recognized as command `export use` + let completion_str = "export use std/ass"; let suggestions = completer.complete(completion_str, completion_str.len()); match_suggestions(&vec!["assert"], &suggestions); diff --git a/crates/nu-lsp/src/ast.rs b/crates/nu-lsp/src/ast.rs index 187d675c24..f8591efb71 100644 --- a/crates/nu-lsp/src/ast.rs +++ b/crates/nu-lsp/src/ast.rs @@ -25,14 +25,14 @@ fn try_find_id_in_misc( location: Option<&usize>, id_ref: Option<&Id>, ) -> Option<(Id, Span)> { - let call_name = working_set.get_span_contents(call.head); + let call_name = working_set.get_decl(call.decl_id).name(); match call_name { - b"def" | b"export def" => try_find_id_in_def(call, working_set, location, id_ref), - b"module" | b"export module" => try_find_id_in_mod(call, working_set, location, id_ref), - b"use" | b"export use" | b"hide" => { + "def" | "export def" => try_find_id_in_def(call, working_set, location, id_ref), + "module" | "export module" => try_find_id_in_mod(call, working_set, location, id_ref), + "use" | "export use" | "hide" => { try_find_id_in_use(call, working_set, location, id_ref, call_name) } - b"overlay use" | b"overlay hide" => { + "overlay use" | "overlay hide" => { try_find_id_in_overlay(call, working_set, location, id_ref) } _ => None, @@ -141,7 +141,7 @@ fn try_find_id_in_use( working_set: &StateWorkingSet, location: Option<&usize>, id: Option<&Id>, - call_name: &[u8], + call_name: &str, ) -> Option<(Id, Span)> { // TODO: for keyword `hide`, the decl/var is already hidden in working_set, // this function will always return None. @@ -176,7 +176,7 @@ fn try_find_id_in_use( if let Some(pos) = location { // first argument of `use` should always be module name // while it is optional in `hide` - if span.contains(*pos) && call_name != b"hide" { + if span.contains(*pos) && call_name != "hide" { return get_matched_module_id(working_set, span, id); } } @@ -196,7 +196,7 @@ fn try_find_id_in_use( }) }; - let arguments = if call_name != b"hide" { + let arguments = if call_name != "hide" { call.arguments.get(1..)? } else { call.arguments.as_slice() diff --git a/crates/nu-lsp/src/workspace.rs b/crates/nu-lsp/src/workspace.rs index 9950a3c0f8..3818834829 100644 --- a/crates/nu-lsp/src/workspace.rs +++ b/crates/nu-lsp/src/workspace.rs @@ -658,7 +658,7 @@ mod tests { let message_num = 5; let messages = - send_reference_request(&client_connection, script.clone(), 6, 11, message_num); + send_reference_request(&client_connection, script.clone(), 6, 12, message_num); assert_eq!(messages.len(), message_num); for message in messages { match message { @@ -676,7 +676,7 @@ mod tests { assert!(array.contains(&serde_json::json!( { "uri": script.to_string(), - "range": { "start": { "line": 6, "character": 12 }, "end": { "line": 6, "character": 19 } } + "range": { "start": { "line": 6, "character": 13 }, "end": { "line": 6, "character": 20 } } } ) )); @@ -712,7 +712,7 @@ mod tests { &client_connection, script.clone(), 6, - 11, + 12, message_num, false, ); @@ -723,8 +723,8 @@ mod tests { Message::Response(r) => assert_json_eq!( r.result, serde_json::json!({ - "start": { "line": 6, "character": 12 }, - "end": { "line": 6, "character": 19 } + "start": { "line": 6, "character": 13 }, + "end": { "line": 6, "character": 20 } }), ), _ => panic!("unexpected message type"), @@ -738,7 +738,7 @@ mod tests { changes[script.to_string()], serde_json::json!([ { - "range": { "start": { "line": 6, "character": 12 }, "end": { "line": 6, "character": 19 } }, + "range": { "start": { "line": 6, "character": 13 }, "end": { "line": 6, "character": 20 } }, "newText": "new" } ]) @@ -860,7 +860,7 @@ mod tests { &client_connection, script.clone(), 6, - 11, + 12, message_num, true, ); diff --git a/tests/fixtures/lsp/workspace/foo.nu b/tests/fixtures/lsp/workspace/foo.nu index 30c4326f68..3af354b032 100644 --- a/tests/fixtures/lsp/workspace/foo.nu +++ b/tests/fixtures/lsp/workspace/foo.nu @@ -4,4 +4,4 @@ export def foooo [ $param } -export def "foo str" [] { "foo" } +export def "foo str" [] { "foo" } From 67b6188b19a5279eaf13a33bfee5b44583c3b9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Riegel?= <96702577+LoicRiegel@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:05:18 +0200 Subject: [PATCH 13/17] feat: into duration accepts floats (#15297) Issue #9887 which can be closed after this is merged. # Description This allows the "into duration" command to accept floats as inputs. Examples: image image **How it works:** Using strings, like `"1.234sec" | into duration`, is already working, so if a user inputs `1.234 | into duration --sec`, I just convert this back to a string and use the previous conversion functions. **Limitations:** there are some limitation to using floats, but it's a general limitation that is already present for other use cases: - only 3 digits are taken into account in the decimal part - floating durations in nano seconds are always floored and not rounded image # User-Facing Changes Users can inject floats with `into duration` # Tests + Formatting cargo fmt and clippy OK Tests OK # After Submitting The example I added will automatically become part of the doc, I think that's enough for documentation. --- .../src/conversions/into/duration.rs | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 129188f839..8c92c22841 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -15,6 +15,7 @@ impl Command for IntoDuration { Signature::build("into duration") .input_output_types(vec![ (Type::Int, Type::Duration), + (Type::Float, Type::Duration), (Type::String, Type::Duration), (Type::Duration, Type::Duration), (Type::table(), Type::table()), @@ -109,6 +110,11 @@ impl Command for IntoDuration { example: "1_234 | into duration --unit ms", result: Some(Value::test_duration(1_234 * 1_000_000)), }, + Example { + description: "Convert a floating point number of an arbitrary unit to duration", + example: "1.234 | into duration --unit sec", + result: Some(Value::test_duration(1_234 * 1_000_000)), + }, ] } } @@ -236,22 +242,22 @@ fn action(input: &Value, unit: &str, span: Span) -> Value { let value_span = input.span(); match input { Value::Duration { .. } => input.clone(), - Value::String { val, .. } => match compound_to_duration(val, value_span) { - Ok(val) => Value::duration(val, span), - Err(error) => Value::error(error, span), - }, + Value::String { val, .. } => { + if let Ok(num) = val.parse::() { + let ns = unit_to_ns_factor(unit); + return Value::duration((num * (ns as f64)) as i64, span); + } + match compound_to_duration(val, value_span) { + Ok(val) => Value::duration(val, span), + Err(error) => Value::error(error, span), + } + } + Value::Float { val, .. } => { + let ns = unit_to_ns_factor(unit); + Value::duration((*val * (ns as f64)) as i64, span) + } Value::Int { val, .. } => { - let ns = match unit { - "ns" => 1, - "us" | "ยตs" => 1_000, - "ms" => 1_000_000, - "sec" => NS_PER_SEC, - "min" => NS_PER_SEC * 60, - "hr" => NS_PER_SEC * 60 * 60, - "day" => NS_PER_SEC * 60 * 60 * 24, - "wk" => NS_PER_SEC * 60 * 60 * 24 * 7, - _ => 0, - }; + let ns = unit_to_ns_factor(unit); Value::duration(*val * ns, span) } // Propagate errors by explicitly matching them before the final case. @@ -268,6 +274,20 @@ fn action(input: &Value, unit: &str, span: Span) -> Value { } } +fn unit_to_ns_factor(unit: &str) -> i64 { + match unit { + "ns" => 1, + "us" | "ยตs" => 1_000, + "ms" => 1_000_000, + "sec" => NS_PER_SEC, + "min" => NS_PER_SEC * 60, + "hr" => NS_PER_SEC * 60 * 60, + "day" => NS_PER_SEC * 60 * 60 * 24, + "wk" => NS_PER_SEC * 60 * 60 * 24 * 7, + _ => 0, + } +} + #[cfg(test)] mod test { use super::*; From 5ec823996abc5b42d9d3abcc9ca51e95162dabc6 Mon Sep 17 00:00:00 2001 From: Wind Date: Thu, 3 Apr 2025 20:08:51 +0800 Subject: [PATCH 14/17] update shadow-rs to version 1 (#15462) # Description Noticed there is a build failure in #15420, because `ShadowBuilder` struct is guarded by `build` feature. This pr is going to update it. # User-Facing Changes Hopefully none. # Tests + Formatting None # After Submitting None --------- Co-authored-by: Stefan Holderbach --- Cargo.lock | 35 +++++++++++++++++++++++++++++++---- crates/nu-cmd-lang/Cargo.toml | 4 ++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8be82f5c9..2995f0e0ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2809,9 +2809,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_debug" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ea828c9d6638a5bd3d8b14e37502b4d56cae910ccf8a5b7f51c7a0eb1d0508" +checksum = "1fe266d2e243c931d8190177f20bf7f24eed45e96f39e87dc49a27b32d12d407" [[package]] name = "is_executable" @@ -6631,13 +6631,14 @@ dependencies = [ [[package]] name = "shadow-rs" -version = "0.38.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d433b5df1e1958a668457ebe4a9c5b7bcfe844f4eb2276ac43cf273baddd54" +checksum = "6d5625ed609cf66d7e505e7d487aca815626dc4ebb6c0dd07637ca61a44651a6" dependencies = [ "const_format", "is_debug", "time", + "tzdb", ] [[package]] @@ -7473,6 +7474,32 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "tz-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1450bf2b99397e72070e7935c89facaa80092ac812502200375f1f7d33c71a1" + +[[package]] +name = "tzdb" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2ea5956f295449f47c0b825c5e109022ff1a6a53bb4f77682a87c2341fbf5" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb_data", +] + +[[package]] +name = "tzdb_data" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0604b35c1f390a774fdb138cac75a99981078895d24bcab175987440bbff803b" +dependencies = [ + "tz-rs", +] + [[package]] name = "ucd-trie" version = "0.1.7" diff --git a/crates/nu-cmd-lang/Cargo.toml b/crates/nu-cmd-lang/Cargo.toml index 62beafd701..c580011981 100644 --- a/crates/nu-cmd-lang/Cargo.toml +++ b/crates/nu-cmd-lang/Cargo.toml @@ -21,10 +21,10 @@ nu-protocol = { path = "../nu-protocol", version = "0.103.1", default-features = nu-utils = { path = "../nu-utils", version = "0.103.1", default-features = false } itertools = { workspace = true } -shadow-rs = { version = "0.38", default-features = false } +shadow-rs = { version = "1.1", default-features = false } [build-dependencies] -shadow-rs = { version = "0.38", default-features = false } +shadow-rs = { version = "1.1", default-features = false, features = ["build"] } [dev-dependencies] quickcheck = { workspace = true } From 2bf0397d806ef108dbe148a09a3abae108b7fe03 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:08:59 -0500 Subject: [PATCH 15/17] bump to the latest rust version (#15483) # Description This PR bumps nushell to use the latest rust version 1.84.1. --- Cargo.toml | 2 +- crates/nu-path/src/trailing_slash.rs | 2 +- .../nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs | 2 +- crates/nu_plugin_query/src/web_tables.rs | 2 +- rust-toolchain.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11762c72a6..9482c837cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ homepage = "https://www.nushell.sh" license = "MIT" name = "nu" repository = "https://github.com/nushell/nushell" -rust-version = "1.83.0" +rust-version = "1.84.1" version = "0.103.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/nu-path/src/trailing_slash.rs b/crates/nu-path/src/trailing_slash.rs index 6a2d08ac63..dd7909dee0 100644 --- a/crates/nu-path/src/trailing_slash.rs +++ b/crates/nu-path/src/trailing_slash.rs @@ -40,7 +40,7 @@ pub fn has_trailing_slash(path: &Path) -> bool { #[cfg(target_arch = "wasm32")] pub fn has_trailing_slash(path: &Path) -> bool { // in the web paths are often just URLs, they are separated by forward slashes - path.to_str().map_or(false, |s| s.ends_with('/')) + path.to_str().is_some_and(|s| s.ends_with('/')) } #[cfg(test)] diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs index de21abafdd..55c1f620c9 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/mod.rs @@ -71,7 +71,7 @@ impl Default for DataFrameValue { impl PartialEq for DataFrameValue { fn eq(&self, other: &Self) -> bool { - self.0.partial_cmp(&other.0).map_or(false, Ordering::is_eq) + self.0.partial_cmp(&other.0).is_some_and(Ordering::is_eq) } } impl Eq for DataFrameValue {} diff --git a/crates/nu_plugin_query/src/web_tables.rs b/crates/nu_plugin_query/src/web_tables.rs index fef8f6c37e..1383c3deaf 100644 --- a/crates/nu_plugin_query/src/web_tables.rs +++ b/crates/nu_plugin_query/src/web_tables.rs @@ -66,7 +66,7 @@ impl WebTable { let mut tables = html .select(&sel_table) .filter(|table| { - table.select(&sel_tr).next().map_or(false, |tr| { + table.select(&sel_tr).next().is_some_and(|tr| { let cells = select_cells(tr, &sel_th, true); if inspect_mode { eprintln!("Potential HTML Headers = {:?}\n", &cells); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2f6272a99b..62c90079fd 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -16,4 +16,4 @@ profile = "default" # use in nushell, we may opt to use the bleeding edge stable version of rust. # I believe rust is on a 6 week release cycle and nushell is on a 4 week release cycle. # So, every two nushell releases, this version number should be bumped by one. -channel = "1.83.0" +channel = "1.84.1" From 237a68560539358a8f41461f87ad9deb07aa2a4e Mon Sep 17 00:00:00 2001 From: Nils Feierabend Date: Fri, 4 Apr 2025 13:35:36 +0200 Subject: [PATCH 16/17] Consider PATH when running command is nuscript in windows (#15486) Fixes #15476 # Description Consider PATH when checking for potential_nuscript_in_windows to allow executing scripts which are in PATH without having to full path address them. It previously only checked the current working directory so only relative paths to cwd and full path worked. The current implementation runs this then through cmd.exe /D /C which can run it with assoc and ftype set for nushell scripts. We could instead run it through nu as `std::env::current_exe()` avoiding the cmd call and the need for assoc and ftype (see: https://github.com/mztikk/nushell/commit/8b25173f02eb5043d796a27f29e19abad69cd6e2). But ive left the current implementation for this intact to not change implementation details, avoid a bigger change and leave this open for discussion here since im not sure if this has any major implications. # User-Facing Changes This would now run every external command through PATH an additional time on windows, so potentially twice. I dont think this has any bigger effect. # Tests + Formatting # After Submitting --- crates/nu-command/src/system/run_external.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 38b9fb165e..03e2d3de02 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -78,6 +78,8 @@ impl Command for External { _ => Path::new(&*name_str).to_owned(), }; + let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; + // On Windows, the user could have run the cmd.exe built-in "assoc" command // Example: "assoc .nu=nuscript" and then run the cmd.exe built-in "ftype" command // Example: "ftype nuscript=C:\path\to\nu.exe '%1' %*" and then added the nushell @@ -88,7 +90,7 @@ impl Command for External { // easy way to do this is to run cmd.exe with the script as an argument. let potential_nuscript_in_windows = if cfg!(windows) { // let's make sure it's a .nu script - if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) { + if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) { let ext = executable .extension() .unwrap_or_default() @@ -133,7 +135,6 @@ impl Command for External { } else { // Determine the PATH to be used and then use `which` to find it - though this has no // effect if it's an absolute path already - let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else { return Err(command_not_found( &name_str, From 7ca2a6f8ace1f5d8e2e9d91df77abe7d1942ef33 Mon Sep 17 00:00:00 2001 From: pyz4 <42039243+pyz4@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:43:21 -0400 Subject: [PATCH 17/17] FIX `polars as-datetime`: ignores timezone information on conversion (#15490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR seeks to fix an error in `polars as-datetime` where timezone information is entirely ignored. This behavior raises a host of silent errors when dealing with datetime conversions (see example below). ## Current Implementation Timezones are entirely ignored and datetimes with different timezones are converted to the same naive datetimes even when the user specifically indicates that the timezone should be parsed. For example, "2021-12-30 00:00:00 +0000" and "2021-12-30 00:00:00 -0400" will both be parsed to "2021-12-30 00:00:00" even when the format string specifically includes "%z". ``` $ ["2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ # โ”‚ datetime โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 0 โ”‚ 12/30/2021 12:00:00AM โ”‚ โ”‚ 1 โ”‚ 12/30/2021 12:00:00AM โ”‚ <-- Same datetime even though the first is +0000 and second is -0400 โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ $ ["2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" | polars schema โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ datetime โ”‚ datetime โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` ## New Implementation Datetimes are converted to UTC and timezone information is retained. ``` $ "2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ # โ”‚ datetime โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 0 โ”‚ 12/30/2021 12:00:00AM โ”‚ โ”‚ 1 โ”‚ 12/30/2021 04:00:00AM โ”‚ <-- Converted to UTC โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ $ ["2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" | polars schema โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ datetime โ”‚ datetime โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` The user may intentionally ignore timezone information by setting the `--naive` flag. ``` $ ["2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" --naive โ•ญโ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ # โ”‚ datetime โ”‚ โ”œโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 0 โ”‚ 12/30/2021 12:00:00AM โ”‚ โ”‚ 1 โ”‚ 12/30/2021 12:00:00AM โ”‚ <-- the -0400 offset is ignored when --naive is set โ•ฐโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ $ ["2021-12-30 00:00:00 +0000" "2021-12-30 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z" --naive | polars schema โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ datetime โ”‚ datetime โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` # User-Facing Changes `polars as-datetime` will now account for timezone information and return type `datetime` rather than `datetime` by default. The user can replicate the previous behavior by setting `--naive`. # Tests + Formatting Tests that incorporated `polars as-datetime` had to be tweaked to include `--naive` flag to replicate previous behavior. # After Submitting --- .../src/dataframe/command/core/to_repr.rs | 34 +++++++------- .../dataframe/command/datetime/as_datetime.rs | 41 ++++++++++++----- .../dataframe/command/datetime/datepart.rs | 17 ++++--- .../values/nu_dataframe/conversion.rs | 45 +++++++++++-------- 4 files changed, 85 insertions(+), 52 deletions(-) diff --git a/crates/nu_plugin_polars/src/dataframe/command/core/to_repr.rs b/crates/nu_plugin_polars/src/dataframe/command/core/to_repr.rs index d867f320cb..417a37cd2e 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/core/to_repr.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/core/to_repr.rs @@ -39,14 +39,14 @@ impl PluginCommand for ToRepr { result: Some(Value::string( r#" shape: (2, 2) -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ” -โ”‚ a โ”† b โ”‚ -โ”‚ --- โ”† --- โ”‚ -โ”‚ datetime[ns] โ”† i64 โ”‚ -โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ก -โ”‚ 2025-01-01 00:00:00 โ”† 2 โ”‚ -โ”‚ 2025-01-02 00:00:00 โ”† 4 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜"# +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ” +โ”‚ a โ”† b โ”‚ +โ”‚ --- โ”† --- โ”‚ +โ”‚ datetime[ns, UTC] โ”† i64 โ”‚ +โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ก +โ”‚ 2025-01-01 00:00:00 UTC โ”† 2 โ”‚ +โ”‚ 2025-01-02 00:00:00 UTC โ”† 4 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜"# .trim(), Span::test_data(), )), @@ -54,18 +54,18 @@ shape: (2, 2) Example { description: "Shows lazy dataframe in repr format", example: - "[[a b]; [2025-01-01 2] [2025-01-02 4]] | polars into-df | polars into-lazy | polars into-repr", + "[[a b]; [2025-01-01 2] [2025-01-02 4]] | polars into-lazy | polars into-repr", result: Some(Value::string( r#" shape: (2, 2) -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ” -โ”‚ a โ”† b โ”‚ -โ”‚ --- โ”† --- โ”‚ -โ”‚ datetime[ns] โ”† i64 โ”‚ -โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ก -โ”‚ 2025-01-01 00:00:00 โ”† 2 โ”‚ -โ”‚ 2025-01-02 00:00:00 โ”† 4 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜"# +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ” +โ”‚ a โ”† b โ”‚ +โ”‚ --- โ”† --- โ”‚ +โ”‚ datetime[ns, UTC] โ”† i64 โ”‚ +โ•žโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ชโ•โ•โ•โ•โ•โ•ก +โ”‚ 2025-01-01 00:00:00 UTC โ”† 2 โ”‚ +โ”‚ 2025-01-02 00:00:00 UTC โ”† 4 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜"# .trim(), Span::test_data(), )), diff --git a/crates/nu_plugin_polars/src/dataframe/command/datetime/as_datetime.rs b/crates/nu_plugin_polars/src/dataframe/command/datetime/as_datetime.rs index a38779927f..3a246cc58c 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/datetime/as_datetime.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/datetime/as_datetime.rs @@ -1,6 +1,7 @@ use crate::{values::CustomValueSupport, PolarsPlugin}; +use std::sync::Arc; -use super::super::super::values::{Column, NuDataFrame}; +use super::super::super::values::{Column, NuDataFrame, NuSchema}; use chrono::DateTime; use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; @@ -8,7 +9,7 @@ use nu_protocol::{ Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, }; -use polars::prelude::{IntoSeries, StringMethods, TimeUnit}; +use polars::prelude::{DataType, Field, IntoSeries, Schema, StringMethods, TimeUnit}; #[derive(Clone)] pub struct AsDateTime; @@ -43,6 +44,7 @@ impl PluginCommand for AsDateTime { Signature::build(self.name()) .required("format", SyntaxShape::String, "formatting date time string") .switch("not-exact", "the format string may be contained in the date (e.g. foo-2021-01-01-bar could match 2021-01-01)", Some('n')) + .switch("naive", "the input datetimes should be parsed as naive (i.e., not timezone-aware)", None) .input_output_type( Type::Custom("dataframe".into()), Type::Custom("dataframe".into()), @@ -54,7 +56,7 @@ impl PluginCommand for AsDateTime { vec![ Example { description: "Converts string to datetime", - example: r#"["2021-12-30 00:00:00" "2021-12-31 00:00:00"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S""#, + example: r#"["2021-12-30 00:00:00 -0400" "2021-12-31 00:00:00 -0400"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S %z""#, result: Some( NuDataFrame::try_from_columns( vec![Column::new( @@ -62,7 +64,7 @@ impl PluginCommand for AsDateTime { vec![ Value::date( DateTime::parse_from_str( - "2021-12-30 00:00:00 +0000", + "2021-12-30 00:00:00 -0400", "%Y-%m-%d %H:%M:%S %z", ) .expect("date calculation should not fail in test"), @@ -70,7 +72,7 @@ impl PluginCommand for AsDateTime { ), Value::date( DateTime::parse_from_str( - "2021-12-31 00:00:00 +0000", + "2021-12-31 00:00:00 -0400", "%Y-%m-%d %H:%M:%S %z", ) .expect("date calculation should not fail in test"), @@ -86,7 +88,7 @@ impl PluginCommand for AsDateTime { }, Example { description: "Converts string to datetime with high resolutions", - example: r#"["2021-12-30 00:00:00.123456789" "2021-12-31 00:00:00.123456789"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S.%9f""#, + example: r#"["2021-12-30 00:00:00.123456789" "2021-12-31 00:00:00.123456789"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S.%9f" --naive"#, result: Some( NuDataFrame::try_from_columns( vec![Column::new( @@ -110,7 +112,15 @@ impl PluginCommand for AsDateTime { ), ], )], - None, + Some(NuSchema::new(Arc::new(Schema::from_iter(vec![ + Field::new( + "datetime".into(), + DataType::Datetime( + TimeUnit::Nanoseconds, + None + ), + ), + ])))), ) .expect("simple df for test should not fail") .into_value(Span::test_data()), @@ -118,7 +128,7 @@ impl PluginCommand for AsDateTime { }, Example { description: "Converts string to datetime using the `--not-exact` flag even with excessive symbols", - example: r#"["2021-12-30 00:00:00 GMT+4"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S" --not-exact"#, + example: r#"["2021-12-30 00:00:00 GMT+4"] | polars into-df | polars as-datetime "%Y-%m-%d %H:%M:%S" --not-exact --naive"#, result: Some( NuDataFrame::try_from_columns( vec![Column::new( @@ -134,7 +144,15 @@ impl PluginCommand for AsDateTime { ), ], )], - None, + Some(NuSchema::new(Arc::new(Schema::from_iter(vec![ + Field::new( + "datetime".into(), + DataType::Datetime( + TimeUnit::Nanoseconds, + None + ), + ), + ])))), ) .expect("simple df for test should not fail") .into_value(Span::test_data()), @@ -162,6 +180,7 @@ fn command( ) -> Result { let format: String = call.req(0)?; let not_exact = call.has_flag("not-exact")?; + let tz_aware = !call.has_flag("naive")?; let df = NuDataFrame::try_from_pipeline_coerce(plugin, input, call.head)?; let series = df.as_series(call.head)?; @@ -177,7 +196,7 @@ fn command( casted.as_datetime_not_exact( Some(format.as_str()), TimeUnit::Nanoseconds, - false, + tz_aware, None, &Default::default(), ) @@ -186,7 +205,7 @@ fn command( Some(format.as_str()), TimeUnit::Nanoseconds, false, - false, + tz_aware, None, &Default::default(), ) diff --git a/crates/nu_plugin_polars/src/dataframe/command/datetime/datepart.rs b/crates/nu_plugin_polars/src/dataframe/command/datetime/datepart.rs index c374ac82f5..1e18639d06 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/datetime/datepart.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/datetime/datepart.rs @@ -1,7 +1,8 @@ use crate::values::NuExpression; +use std::sync::Arc; use crate::{ - dataframe::values::{Column, NuDataFrame}, + dataframe::values::{Column, NuDataFrame, NuSchema}, values::CustomValueSupport, PolarsPlugin, }; @@ -13,7 +14,7 @@ use nu_protocol::{ }; use polars::{ datatypes::{DataType, TimeUnit}, - prelude::NamedFrom, + prelude::{Field, NamedFrom, Schema}, series::Series, }; @@ -54,14 +55,20 @@ impl PluginCommand for ExprDatePart { vec![ Example { description: "Creates an expression to capture the year date part", - example: r#"[["2021-12-30T01:02:03.123456789"]] | polars into-df | polars as-datetime "%Y-%m-%dT%H:%M:%S.%9f" | polars with-column [(polars col datetime | polars datepart year | polars as datetime_year )]"#, + example: r#"[["2021-12-30T01:02:03.123456789"]] | polars into-df | polars as-datetime "%Y-%m-%dT%H:%M:%S.%9f" --naive | polars with-column [(polars col datetime | polars datepart year | polars as datetime_year )]"#, result: Some( NuDataFrame::try_from_columns( vec![ Column::new("datetime".to_string(), vec![Value::test_date(dt)]), Column::new("datetime_year".to_string(), vec![Value::test_int(2021)]), ], - None, + Some(NuSchema::new(Arc::new(Schema::from_iter(vec![ + Field::new( + "datetime".into(), + DataType::Datetime(TimeUnit::Nanoseconds, None), + ), + Field::new("datetime_year".into(), DataType::Int64), + ])))), ) .expect("simple df for test should not fail") .into_value(Span::test_data()), @@ -69,7 +76,7 @@ impl PluginCommand for ExprDatePart { }, Example { description: "Creates an expression to capture multiple date parts", - example: r#"[["2021-12-30T01:02:03.123456789"]] | polars into-df | polars as-datetime "%Y-%m-%dT%H:%M:%S.%9f" | + example: r#"[["2021-12-30T01:02:03.123456789"]] | polars into-df | polars as-datetime "%Y-%m-%dT%H:%M:%S.%9f" --naive | polars with-column [ (polars col datetime | polars datepart year | polars as datetime_year ), (polars col datetime | polars datepart month | polars as datetime_month ), (polars col datetime | polars datepart day | polars as datetime_day ), diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/conversion.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/conversion.rs index 82051f7b1b..8b898e449a 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/conversion.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/conversion.rs @@ -245,7 +245,10 @@ fn value_to_data_type(value: &Value) -> Option { Value::Float { .. } => Some(DataType::Float64), Value::String { .. } => Some(DataType::String), Value::Bool { .. } => Some(DataType::Boolean), - Value::Date { .. } => Some(DataType::Date), + Value::Date { .. } => Some(DataType::Datetime( + TimeUnit::Nanoseconds, + Some(PlSmallStr::from_static("UTC")), + )), Value::Duration { .. } => Some(DataType::Duration(TimeUnit::Nanoseconds)), Value::Filesize { .. } => Some(DataType::Int64), Value::Binary { .. } => Some(DataType::Binary), @@ -447,24 +450,28 @@ fn typed_column_to_series(name: PlSmallStr, column: TypedColumn) -> Result().map(|tz| val.with_timezone(&tz))) - .transpose() - .map_err(|e| ShellError::GenericError { - error: "Error parsing timezone".into(), - msg: "".into(), - span: None, - help: Some(e.to_string()), - inner: vec![], - })? - .and_then(|dt| dt.timestamp_nanos_opt()) - .map(|nanos| nanos_from_timeunit(nanos, *tu))) - } else { - Ok(None) + match (maybe_tz, &v) { + (Some(tz), Value::Date { val, .. }) => { + // If there is a timezone specified, make sure + // the value is converted to it + Ok(tz + .parse::() + .map(|tz| val.with_timezone(&tz)) + .map_err(|e| ShellError::GenericError { + error: "Error parsing timezone".into(), + msg: "".into(), + span: None, + help: Some(e.to_string()), + inner: vec![], + })? + .timestamp_nanos_opt() + .map(|nanos| nanos_from_timeunit(nanos, *tu))) + } + (None, Value::Date { val, .. }) => Ok(val + .timestamp_nanos_opt() + .map(|nanos| nanos_from_timeunit(nanos, *tu))), + + _ => Ok(None), } }) .collect::>, ShellError>>()?;