From 0cd90e2388cce0cb66a503898863aa57aa2ce6bb Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Sat, 5 Apr 2025 16:26:50 +0300 Subject: [PATCH 01/13] Fix #15394 for `table -e` wrapping issue (#15407) close #15394 cc @fdncred --- crates/nu-command/tests/commands/table.rs | 386 ++++++++++++---- crates/nu-table/src/table.rs | 518 ++++++++-------------- crates/nu-table/src/types/expanded.rs | 11 +- crates/nu-table/src/util.rs | 21 + typos.toml | 1 + 5 files changed, 526 insertions(+), 411 deletions(-) diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index cc40a41a74..6bd7dfbc00 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -1333,7 +1333,15 @@ fn test_expand_big_0() { "│ target │ {record 3 fields} │", "│ dev-dependencies │ {record 9 fields} │", "│ features │ {record 8 fields} │", - "│ bin │ [table 1 row] │", + "│ │ ╭───┬─────┬─────╮ │", + "│ bin │ │ # │ nam │ pat │ │", + "│ │ │ │ e │ h │ │", + "│ │ ├───┼─────┼─────┤ │", + "│ │ │ 0 │ nu │ src │ │", + "│ │ │ │ │ /ma │ │", + "│ │ │ │ │ in. │ │", + "│ │ │ │ │ rs │ │", + "│ │ ╰───┴─────┴─────╯ │", "│ │ ╭───────────┬───╮ │", "│ patch │ │ crates-io │ { │ │", "│ │ │ │ r │ │", @@ -1352,7 +1360,16 @@ fn test_expand_big_0() { "│ │ │ │ d │ │", "│ │ │ │ } │ │", "│ │ ╰───────────┴───╯ │", - "│ bench │ [table 1 row] │", + "│ │ ╭───┬─────┬─────╮ │", + "│ bench │ │ # │ nam │ har │ │", + "│ │ │ │ e │ nes │ │", + "│ │ │ │ │ s │ │", + "│ │ ├───┼─────┼─────┤ │", + "│ │ │ 0 │ ben │ fal │ │", + "│ │ │ │ chm │ se │ │", + "│ │ │ │ ark │ │ │", + "│ │ │ │ s │ │ │", + "│ │ ╰───┴─────┴─────╯ │", "╰──────────────────┴───────────────────╯", ]); @@ -1366,6 +1383,8 @@ fn table_expande_with_no_header_internally_0() { let actual = nu!(format!("{} | table --expand --width 141", nu_value.trim())); + _print_lines(&actual.out, 141); + assert_eq!( actual.out, join_lines([ @@ -1532,71 +1551,191 @@ fn table_expande_with_no_header_internally_0() { "│ │ │ │ │ │ ╰─────┴──────────╯ │ │ │", "│ │ │ │ │ display_output │ │ │ │", "│ │ │ │ ╰────────────────┴────────────────────╯ │ │", - "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬─────╮ │ │", - "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ ... │ │ │", - "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ false │ | │ ... │ │ │", - "│ │ │ │ │ 1 │ history_menu │ true │ ? │ ... │ │ │", - "│ │ │ │ │ 2 │ help_menu │ true │ ? │ ... │ │ │", - "│ │ │ │ │ 3 │ commands_menu │ false │ # │ ... │ │ │", - "│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │", - "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │", - "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴─────╯ │ │", + "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬───┬─────╮ │ │", + "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ t │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ y │ │ │ │", + "│ │ │ │ │ │ │ │ │ p │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼───┼─────┤ │ │", + "│ │ │ │ │ 0 │ completion_menu │ false │ | │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 4 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ │ 1 │ history_menu │ true │ ? │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 2 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ │ 2 │ help_menu │ true │ ? │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 6 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ │ 3 │ commands_menu │ false │ # │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 4 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ │ 4 │ vars_menu │ true │ # │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 2 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ { │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ c │ │ │ │", + "│ │ │ │ │ │ │ │ │ o │ │ │ │", + "│ │ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ 6 │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ f │ │ │ │", + "│ │ │ │ │ │ │ │ │ i │ │ │ │", + "│ │ │ │ │ │ │ │ │ e │ │ │ │", + "│ │ │ │ │ │ │ │ │ l │ │ │ │", + "│ │ │ │ │ │ │ │ │ d │ │ │ │", + "│ │ │ │ │ │ │ │ │ s │ │ │ │", + "│ │ │ │ │ │ │ │ │ } │ │ │ │", + "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴───┴─────╯ │ │", "│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬───────────────┬─────╮ │ │", - "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ... │ │ │", + "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │", + "│ │ │ │ │ │ │ │ │ │ nt │ │ │", "│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼───────────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ ... │ │ │", - "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ ... │ │ │", - "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ ... │ │ │", - "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ ... │ │ │", - "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ s} │ │ │", + "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───────╮ │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", + "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───────╮ │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", "│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴───────────────┴─────╯ │ │", "│ │ ╰──────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────╯ │", @@ -1611,6 +1750,8 @@ fn table_expande_with_no_header_internally_1() { let actual = nu!(format!("{} | table --expand --width 136", nu_value.trim())); + _print_lines(&actual.out, 136); + assert_eq!( actual.out, join_lines([ @@ -1777,37 +1918,87 @@ fn table_expande_with_no_header_internally_1() { "│ │ │ │ │ │ ╰─────┴──────────╯ │ │ │", "│ │ │ │ │ display_output │ │ │ │", "│ │ │ │ ╰────────────────┴────────────────────╯ │ │", - "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬─────╮ │ │", - "│ │ │ menus │ │ # │ name │ only_buffer_difference │ ... │ │ │", - "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ false │ ... │ │ │", - "│ │ │ │ │ 1 │ history_menu │ true │ ... │ │ │", - "│ │ │ │ │ 2 │ help_menu │ true │ ... │ │ │", - "│ │ │ │ │ 3 │ commands_menu │ false │ ... │ │ │", - "│ │ │ │ │ 4 │ vars_menu │ true │ ... │ │ │", - "│ │ │ │ │ 5 │ commands_with_description │ true │ ... │ │ │", - "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴─────╯ │ │", + "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬───────┬─────╮ │ │", + "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marke │ ... │ │ │", + "│ │ │ │ │ │ │ │ r │ │ │ │", + "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼───────┼─────┤ │ │", + "│ │ │ │ │ 0 │ completion_menu │ false │ | │ ... │ │ │", + "│ │ │ │ │ 1 │ history_menu │ true │ ? │ ... │ │ │", + "│ │ │ │ │ 2 │ help_menu │ true │ ? │ ... │ │ │", + "│ │ │ │ │ 3 │ commands_menu │ false │ # │ ... │ │ │", + "│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │", + "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │", + "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴───────┴─────╯ │ │", "│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬──────────┬─────╮ │ │", - "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ... │ │ │", + "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │", + "│ │ │ │ │ │ │ │ │ │ nt │ │ │", "│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼──────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ ... │ │ │", - "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ ... │ │ │", - "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ ... │ │ │", - "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ ... │ │ │", - "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", - "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ [list 3 │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ s} │ │ │", + "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ } │ │ │", + "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ s} │ │ │", + "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ s} │ │ │", + "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ [list 3 │ {re │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", + "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", + "│ │ │ │ │ │ │ │ │ │ fi │ │ │", + "│ │ │ │ │ │ │ │ │ │ eld │ │ │", + "│ │ │ │ │ │ │ │ │ │ s} │ │ │", "│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴──────────┴─────╯ │ │", "│ │ ╰──────────────────────────────────┴──────────────────────────────────────────────────────────────────────────╯ │", "╰────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", @@ -2521,6 +2712,7 @@ fn table_theme_on_border_with_love() { fn table_theme_on_border_thin() { assert_eq!( create_theme_output("thin"), + // ["┌─#─┬a_looooooong_name┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴a_looooooong_name┴─b─┴─c─┘"] [ "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└───┴───┴───┴────────────────┘", "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└─#─┴─a─┴─b─┴───────c────────┘", @@ -3149,3 +3341,21 @@ fn table_index_expand() { ╰─────┴─────╯" ); } + +#[test] +fn table_expand_big_header() { + let actual = nu!(" + let column_name = (('' | fill -c 'a' --width 81)) + [{ $column_name: 'contents' }] | table -e --width=80 + "); + + assert_eq!( + actual.out, + "╭───┬──────────────────────────────────────────────────────────────────────────╮\ + │ # │ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │\ + │ │ aaaaaaaaa │\ + ├───┼──────────────────────────────────────────────────────────────────────────┤\ + │ 0 │ contents │\ + ╰───┴──────────────────────────────────────────────────────────────────────────╯" + ); +} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index b706a41eb2..90d23e0269 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -3,33 +3,30 @@ use std::{cmp::min, collections::HashMap}; use nu_ansi_term::Style; use nu_color_config::TextStyle; use nu_protocol::{TableIndent, TrimStrategy}; -use nu_utils::strip_ansi_unlikely; use tabled::{ builder::Builder, grid::{ ansi::ANSIBuf, - colors::Colors, config::{AlignmentHorizontal, ColoredConfig, Entity, Position}, dimension::CompleteDimensionVecRecords, records::{ vec_records::{Cell, Text, VecRecords}, - ExactRecords, Records, Resizable, + ExactRecords, Records, }, }, settings::{ - format::FormatContent, formatting::AlignmentStrategy, - object::{Columns, Row, Rows}, + object::{Columns, Rows}, peaker::Priority, themes::ColumnNames, width::Truncate, - Alignment, Color, Format, Modify, ModifyList, Padding, Settings, TableOption, Width, + Alignment, Color, Modify, Padding, TableOption, Width, }, Table, }; -use crate::{convert_style, is_color_empty, table_theme::TableTheme}; +use crate::{convert_style, is_color_empty, string_width, table_theme::TableTheme}; pub type NuRecords = VecRecords; pub type NuRecordsValue = Text; @@ -244,14 +241,41 @@ impl TableStructure { } } +#[derive(Debug, Clone)] +struct HeadInfo { + values: Vec, + align: AlignmentHorizontal, + color: Option, +} + fn build_table(mut t: NuTable, termwidth: usize) -> Option { if t.count_columns() == 0 || t.count_rows() == 0 { return Some(String::new()); } - let widths = table_truncate(&mut t, termwidth)?; - table_insert_footer(&mut t); - draw_table(t, widths, termwidth) + let mut head = None; + if is_header_on_border(&t) { + head = Some(remove_header(&mut t)); + } else { + table_insert_footer(&mut t); + } + + let widths = table_truncate(&mut t, head.clone(), termwidth)?; + if let Some(head) = head.as_mut() { + if head.values.len() > widths.len() { + head.values[widths.len() - 1] = String::from("..."); + } + } + + draw_table(t, widths, head, termwidth) +} + +fn is_header_on_border(t: &NuTable) -> bool { + let structure = get_table_structure(&t.data, &t.config); + let is_configured = structure.with_header && t.config.header_on_border; + let has_horizontal = t.config.theme.as_base().borders_has_top() + || t.config.theme.as_base().get_horizontal_line(1).is_some(); + is_configured && has_horizontal } fn table_insert_footer(t: &mut NuTable) { @@ -260,9 +284,8 @@ fn table_insert_footer(t: &mut NuTable) { } } -fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option> { - let pad = t.config.indent.left + t.config.indent.right; - let widths = maybe_truncate_columns(&mut t.data, &t.config, termwidth, pad); +fn table_truncate(t: &mut NuTable, head: Option, termwidth: usize) -> Option> { + let widths = maybe_truncate_columns(&mut t.data, &t.config, head, termwidth); if widths.is_empty() { return None; } @@ -270,10 +293,52 @@ fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option> { Some(widths) } -fn draw_table(t: NuTable, widths: Vec, termwidth: usize) -> Option { - let structure = get_table_structure(&t.data, &t.config); +fn remove_header(t: &mut NuTable) -> HeadInfo { + let head: Vec = t + .data + .remove(0) + .into_iter() + .map(|s| s.to_string()) + .collect(); + let align = t.alignments.header; + let color = is_color_empty(&t.styles.header).then(|| t.styles.header.clone()); + + // move settings by one row down + t.alignments.cells = t + .alignments + .cells + .drain() + .filter(|(k, _)| k.0 != 0) + .map(|(k, v)| ((k.0 - 1, k.1), v)) + .collect(); + + // move settings by one row down + t.styles.cells = t + .styles + .cells + .drain() + .filter(|(k, _)| k.0 != 0) + .map(|(k, v)| ((k.0 - 1, k.1), v)) + .collect(); + + HeadInfo { + values: head, + align, + color, + } +} + +fn draw_table( + t: NuTable, + widths: Vec, + head: Option, + termwidth: usize, +) -> Option { + let mut structure = get_table_structure(&t.data, &t.config); let sep_color = t.config.border_color; - let border_header = structure.with_header && t.config.header_on_border; + if head.is_some() { + structure.with_header = false; + } let data: Vec> = t.data.into(); let mut table = Builder::from_vec(data).build(); @@ -282,15 +347,51 @@ fn draw_table(t: NuTable, widths: Vec, termwidth: usize) -> Option, + theme: &TableTheme, + structure: TableStructure, +) { + let head = match head { + Some(head) => head, + None => return, + }; + + let mut widths = GetDims(Vec::new()); + table.with(&mut widths); + + if !theme.as_base().borders_has_top() { + let line = theme.as_base().get_horizontal_line(1); + if let Some(line) = line.cloned() { + table.get_config_mut().insert_horizontal_line(0, line); + if structure.with_footer { + let last_row = table.count_rows(); + table + .get_config_mut() + .insert_horizontal_line(last_row, line); + } + }; + } + + if structure.with_footer { + let last_row = table.count_rows(); + table.with(SetLineHeaders::new(last_row, head.clone())); + } + + table.with(SetLineHeaders::new(0, head)); +} + +fn truncate_table(table: &mut Table, cfg: TableConfig, widths: Vec, termwidth: usize) { + table.with(WidthCtrl::new(widths, cfg, termwidth)); +} + fn indent_sum(indent: TableIndent) -> usize { indent.left + indent.right } @@ -303,75 +404,10 @@ fn get_table_structure(data: &VecRecords>, cfg: &TableConfig) -> Ta TableStructure::new(with_index, with_header, with_footer) } -fn adjust_table(table: &mut Table, width_ctrl: WidthCtrl, border_header: bool, with_footer: bool) { - if border_header { - if with_footer { - set_border_head_with_footer(table, width_ctrl); - } else { - set_border_head(table, width_ctrl); - } - } else { - table.with(width_ctrl); - } -} - fn set_indent(table: &mut Table, indent: TableIndent) { table.with(Padding::new(indent.left, indent.right, 0, 0)); } -fn set_border_head(table: &mut Table, wctrl: WidthCtrl) { - let mut row = GetRow(0, Vec::new()); - let mut row_opts = GetRowSettings(0, AlignmentHorizontal::Left, None); - - table.with(&mut row); - table.with(&mut row_opts); - - table.with( - Settings::default() - .with(strip_color_from_row(0)) - .with(wctrl) - .with(MoveRowNext::new(0, 0)) - .with(SetLineHeaders::new(0, row.1, row_opts.1, row_opts.2)), - ); -} - -fn set_border_head_with_footer(table: &mut Table, wctrl: WidthCtrl) { - // note: funnily last and row must be equal at this point but we do not rely on it just in case. - - let count_rows = table.count_rows(); - let last_row_index = count_rows - 1; - - let mut first_row = GetRow(0, Vec::new()); - let mut head_settings = GetRowSettings(0, AlignmentHorizontal::Left, None); - let mut last_row = GetRow(last_row_index, Vec::new()); - - table.with(&mut first_row); - table.with(&mut head_settings); - table.with(&mut last_row); - - let head = first_row.1; - let footer = last_row.1; - let alignment = head_settings.1; - let head_color = head_settings.2.clone(); - let footer_color = head_settings.2; - - table.with( - Settings::default() - .with(strip_color_from_row(0)) - .with(strip_color_from_row(count_rows - 1)) - .with(wctrl) - .with(MoveRowNext::new(0, 0)) - .with(MoveRowPrev::new(last_row_index - 1, last_row_index)) - .with(SetLineHeaders::new(0, head, alignment, head_color)) - .with(SetLineHeaders::new( - last_row_index - 1, - footer, - alignment, - footer_color, - )), - ); -} - fn table_to_string(table: Table, termwidth: usize) -> Option { let total_width = table.total_width(); @@ -387,16 +423,14 @@ struct WidthCtrl { width: Vec, cfg: TableConfig, width_max: usize, - pad: usize, } impl WidthCtrl { - fn new(width: Vec, cfg: TableConfig, max: usize, pad: usize) -> Self { + fn new(width: Vec, cfg: TableConfig, max: usize) -> Self { Self { width, cfg, width_max: max, - pad, } } } @@ -414,8 +448,9 @@ impl TableOption> for if need_truncation { let has_header = self.cfg.structure.with_header && rec.count_rows() > 1; let as_head = has_header && self.cfg.header_on_border; + let pad = indent_sum(self.cfg.indent); - let trim = TableTrim::new(self.width, self.width_max, self.cfg.trim, as_head, self.pad); + let trim = TableTrim::new(self.width, self.width_max, self.cfg.trim, as_head, pad); trim.change(rec, cfg, dim); return; } @@ -663,24 +698,22 @@ fn load_theme( fn maybe_truncate_columns( data: &mut NuRecords, cfg: &TableConfig, + head: Option, termwidth: usize, - pad: usize, ) -> Vec { const TERMWIDTH_THRESHOLD: usize = 120; + let pad = cfg.indent.left + cfg.indent.right; + let preserve_content = termwidth > TERMWIDTH_THRESHOLD; - let has_header = cfg.structure.with_header && data.count_rows() > 1; - let is_header_on_border = has_header && cfg.header_on_border; - let truncate = if is_header_on_border { - truncate_columns_by_head + if let Some(head) = head { + truncate_columns_by_head(data, &cfg.theme, head, pad, termwidth) } else if preserve_content { - truncate_columns_by_columns + truncate_columns_by_columns(data, &cfg.theme, pad, termwidth) } else { - truncate_columns_by_content - }; - - truncate(data, &cfg.theme, pad, termwidth) + truncate_columns_by_content(data, &cfg.theme, pad, termwidth) + } } // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. @@ -835,51 +868,76 @@ fn truncate_columns_by_columns( widths } -// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. +// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but +// WITH AS MUCH CONTENT AS POSSIBLE BY ACCOUNTED BY HEADERS. fn truncate_columns_by_head( data: &mut NuRecords, theme: &TableTheme, + head: HeadInfo, pad: usize, termwidth: usize, ) -> Vec { + const MIN_ACCEPTABLE_WIDTH: usize = 3; const TRAILING_COLUMN_WIDTH: usize = 5; - let config = create_config(theme, false, None); - let mut widths = build_width(&*data, pad); - let total_width = get_total_width2(&widths, &config); - if total_width <= termwidth { - return widths; - } - if data.is_empty() { - return widths; + return vec![0; data.count_columns()]; } - let head = &data[0]; + let mut widths = build_width(data, pad); + let config = create_config(theme, false, None); let borders = config.get_borders(); let has_vertical = borders.has_vertical(); let mut width = borders.has_left() as usize + borders.has_right() as usize; let mut truncate_pos = 0; - for (i, column_header) in head.iter().enumerate() { - let column_header_width = Cell::width(column_header); - width += column_header_width + pad; + for (i, head) in head.values.iter().enumerate() { + let head_width = string_width(head); + let col_width = widths[i]; + if head_width + pad <= col_width { + let move_width = head_width + pad + (i > 0 && has_vertical) as usize; + if width + move_width >= termwidth { + break; + } - if i > 0 { - width += has_vertical as usize; + width += move_width; + truncate_pos += 1; + continue; } - if width >= termwidth { - width -= column_header_width + (i > 0 && has_vertical) as usize + pad; + // NOTE: So header is bigger then a column + // Therefore we must try to expand the column to head text width as much as possible. + // + // The kicker is that we will truncate the header if we can't fit it totally. + // Therefore it's not guaranteed that the column will be expanded to exactly head width. + widths[i] = head_width + pad; + let col_width = widths[i]; + + let move_width = col_width + (i > 0 && has_vertical) as usize; + if width + move_width >= termwidth { + let mut used_width = width + pad + (i > 0 && has_vertical) as usize; + if i + 1 != widths.len() { + used_width += TRAILING_COLUMN_WIDTH; + } + + let available = termwidth.saturating_sub(used_width); + + if available > MIN_ACCEPTABLE_WIDTH { + width += available; + widths[i] = available; + truncate_pos += 1; + } + break; } + width += move_width; truncate_pos += 1; } // we don't need any truncation then (is it possible?) - if truncate_pos == head.len() { + if truncate_pos == head.values.len() { return widths; } @@ -991,63 +1049,16 @@ fn build_width(records: &NuRecords, pad: usize) -> Vec { widths } -struct GetRow(usize, Vec); - -impl TableOption> for &mut GetRow { - fn change( - self, - recs: &mut NuRecords, - _: &mut ColoredConfig, - _: &mut CompleteDimensionVecRecords<'_>, - ) { - let row = self.0; - self.1 = recs[row].iter().map(|c| c.as_ref().to_owned()).collect(); - } -} - -struct GetRowSettings(usize, AlignmentHorizontal, Option); - -impl TableOption> - for &mut GetRowSettings -{ - fn change( - self, - _: &mut NuRecords, - cfg: &mut ColoredConfig, - _: &mut CompleteDimensionVecRecords<'_>, - ) { - let row = self.0; - self.1 = *cfg.get_alignment_horizontal(Entity::Row(row)); - self.2 = cfg - .get_colors() - .get_color((row, 0)) - .cloned() - .map(Color::from); - } -} - // It's laverages a use of guuaranted cached widths before hand // to speed up things a bit. struct SetLineHeaders { line: usize, - columns: Vec, - alignment: AlignmentHorizontal, - color: Option, + head: HeadInfo, } impl SetLineHeaders { - fn new( - line: usize, - columns: Vec, - alignment: AlignmentHorizontal, - color: Option, - ) -> Self { - Self { - line, - columns, - alignment, - color, - } + fn new(line: usize, head: HeadInfo) -> Self { + Self { line, head } } } @@ -1071,7 +1082,8 @@ impl TableOption> for }; let columns: Vec<_> = self - .columns + .head + .values .into_iter() .zip(widths.iter().cloned()) // it must be always safe to do .map(|(s, width)| Truncate::truncate(&s, width).into_owned()) @@ -1079,8 +1091,8 @@ impl TableOption> for let mut names = ColumnNames::new(columns) .line(self.line) - .alignment(Alignment::from(self.alignment)); - if let Some(color) = self.color { + .alignment(Alignment::from(self.head.align)); + if let Some(color) = self.head.color { names = names.color(color); } @@ -1092,163 +1104,25 @@ impl TableOption> for } } -struct MoveRowNext { - row: usize, - line: usize, -} - -impl MoveRowNext { - fn new(row: usize, line: usize) -> Self { - Self { row, line } - } -} - -struct MoveRowPrev { - row: usize, - line: usize, -} - -impl MoveRowPrev { - fn new(row: usize, line: usize) -> Self { - Self { row, line } - } -} - -impl TableOption> for MoveRowNext { - fn change( - self, - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - _: &mut CompleteDimensionVecRecords<'_>, - ) { - row_shift_next(recs, cfg, self.row, self.line); - } - - fn hint_change(&self) -> Option { - None - } -} - -impl TableOption> for MoveRowPrev { - fn change( - self, - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - _: &mut CompleteDimensionVecRecords<'_>, - ) { - row_shift_prev(recs, cfg, self.row, self.line); - } - - fn hint_change(&self) -> Option { - None - } -} - -fn row_shift_next(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { - let count_rows = recs.count_rows(); - let count_columns = recs.count_columns(); - let has_line = cfg.has_horizontal(line, count_rows); - let has_next_line = cfg.has_horizontal(line + 1, count_rows); - if !has_line && !has_next_line { - return; - } - - recs.remove_row(row); - let count_rows = recs.count_rows(); - - shift_alignments_down(cfg, row, count_rows, count_columns); - shift_colors_down(cfg, row, count_rows, count_columns); - - if !has_line { - shift_lines_up(cfg, count_rows, &[line + 1]); - } else { - remove_lines(cfg, count_rows, &[line + 1]); - } - - shift_lines_up(cfg, count_rows, &[count_rows]); -} - -fn row_shift_prev(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { - let mut count_rows = recs.count_rows(); - let count_columns = recs.count_columns(); - let has_line = cfg.has_horizontal(line, count_rows); - let has_prev_line = cfg.has_horizontal(line - 1, count_rows); - if !has_line && !has_prev_line { - return; - } - - recs.remove_row(row); - - if !has_line { - return; - } - - count_rows -= 1; - - shift_alignments_down(cfg, row, count_rows, count_columns); - shift_colors_down(cfg, row, count_rows, count_columns); - remove_lines(cfg, count_rows, &[line - 1]); -} - -fn remove_lines(cfg: &mut ColoredConfig, count_rows: usize, line: &[usize]) { - for &line in line { - cfg.remove_horizontal_line(line, count_rows) - } -} - -fn shift_alignments_down( - cfg: &mut ColoredConfig, - row: usize, - count_rows: usize, - count_columns: usize, -) { - for row in row..count_rows { - for col in 0..count_columns { - let pos = (row + 1, col).into(); - let posn = (row, col).into(); - let align = *cfg.get_alignment_horizontal(pos); - cfg.set_alignment_horizontal(posn, align); - } - - let align = *cfg.get_alignment_horizontal(Entity::Row(row + 1)); - cfg.set_alignment_horizontal(Entity::Row(row), align); - } -} - -fn shift_colors_down(cfg: &mut ColoredConfig, row: usize, count_rows: usize, count_columns: usize) { - for row in row..count_rows { - for col in 0..count_columns { - let pos = (row + 1, col); - let posn = (row, col).into(); - let color = cfg.get_colors().get_color(pos).cloned(); - if let Some(color) = color { - cfg.set_color(posn, color); - } - } - } -} - -fn shift_lines_up(cfg: &mut ColoredConfig, count_rows: usize, lines: &[usize]) { - for &i in lines { - let line = cfg.get_horizontal_line(i).cloned(); - if let Some(line) = line { - cfg.insert_horizontal_line(i - 1, line); - cfg.remove_horizontal_line(i, count_rows); - } - } -} - fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) { if let Some(line) = theme.get_horizontal_line(from) { theme.insert_horizontal_line(to, *line); } } -#[allow(clippy::type_complexity)] -fn strip_color_from_row(row: usize) -> ModifyList String>> { - fn foo(s: &str) -> String { - strip_ansi_unlikely(s).into_owned() +struct GetDims(Vec); + +impl TableOption> for &mut GetDims { + fn change( + self, + _: &mut NuRecords, + _: &mut ColoredConfig, + dims: &mut CompleteDimensionVecRecords<'_>, + ) { + self.0 = dims.get_widths().expect("expected to get it").to_vec(); } - Modify::new(Rows::single(row)).with(Format::content(foo)) + fn hint_change(&self) -> Option { + None + } } diff --git a/crates/nu-table/src/types/expanded.rs b/crates/nu-table/src/types/expanded.rs index 393a381646..c93832fc19 100644 --- a/crates/nu-table/src/types/expanded.rs +++ b/crates/nu-table/src/types/expanded.rs @@ -240,7 +240,7 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } let mut available = available_width - pad_space; - let mut column_width = string_width(&header); + let mut column_width = 0; if !is_last_column { // we need to make sure that we have a space for a next column if we use available width @@ -293,9 +293,18 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { column_rows = column_rows.saturating_add(cell.size); } + let mut head_width = string_width(&header); + let mut header = header; + if head_width > available { + header = wrap_text(&header, available, cfg.opts.config); + head_width = available; + } + let head_cell = NuRecordsValue::new(header); data[0].push(head_cell); + column_width = max(column_width, head_width); + if column_width > available { // remove the column we just inserted for row in &mut data { diff --git a/crates/nu-table/src/util.rs b/crates/nu-table/src/util.rs index 8384ef144f..8784454cfb 100644 --- a/crates/nu-table/src/util.rs +++ b/crates/nu-table/src/util.rs @@ -31,6 +31,27 @@ pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String { Wrap::wrap(text, width, keep_words) } +pub fn string_expand(text: &str, width: usize) -> String { + use std::{borrow::Cow, iter::repeat}; + use tabled::grid::util::string::{get_line_width, get_lines}; + + get_lines(text) + .map(|line| { + let length = get_line_width(&line); + + if length < width { + let mut line = line.into_owned(); + let remain = width - length; + line.extend(repeat(' ').take(remain)); + Cow::Owned(line) + } else { + line + } + }) + .collect::>() + .join("\n") +} + pub fn string_truncate(text: &str, width: usize) -> String { let line = match text.lines().next() { Some(line) => line, diff --git a/typos.toml b/typos.toml index 14baa39811..76d8b8959a 100644 --- a/typos.toml +++ b/typos.toml @@ -1,6 +1,7 @@ [files] extend-exclude = [ ".git/", + "crates/nu-command/tests/commands/table.rs", "crates/nu-cmd-extra/assets/228_themes.json", "tests/fixtures/formats/", ] From 210c6f1c43718698af7467a918e1b70dbace002d Mon Sep 17 00:00:00 2001 From: zc he Date: Sat, 5 Apr 2025 21:41:34 +0800 Subject: [PATCH 02/13] fix(lsp): more accurate PWD: from env -> parent dir of current file (#15470) # Description Some editors like neovim will provide "workspace root" as PWD, which can mess up file completion results. # User-Facing Changes bug fix # Tests + Formatting adjusted # After Submitting --- crates/nu-lsp/src/completion.rs | 10 +-- crates/nu-lsp/src/diagnostics.rs | 2 +- crates/nu-lsp/src/goto.rs | 3 +- crates/nu-lsp/src/hover.rs | 3 +- crates/nu-lsp/src/lib.rs | 36 +++++--- crates/nu-lsp/src/signature.rs | 2 +- crates/nu-lsp/src/symbols.rs | 4 +- crates/nu-lsp/src/workspace.rs | 103 ++++++++++++++++------ tests/fixtures/lsp/completion/command.nu | 2 +- tests/fixtures/lsp/completion/fallback.nu | 2 +- 10 files changed, 110 insertions(+), 57 deletions(-) diff --git a/crates/nu-lsp/src/completion.rs b/crates/nu-lsp/src/completion.rs index 110f5d4f0f..e7573857f9 100644 --- a/crates/nu-lsp/src/completion.rs +++ b/crates/nu-lsp/src/completion.rs @@ -29,7 +29,7 @@ impl LanguageServer { .is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c)); self.need_parse |= need_fallback; - let engine_state = Arc::new(self.new_engine_state()); + let engine_state = Arc::new(self.new_engine_state(Some(&path_uri))); let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); let results = if need_fallback { completer.fetch_completions_at(&file_text[..location], location) @@ -264,10 +264,10 @@ mod tests { let resp = send_complete_request(&client_connection, script.clone(), 2, 18); assert!(result_from_message(resp).as_array().unwrap().contains( &serde_json::json!({ - "label": "LICENSE", + "label": "command.nu", "labelDetails": { "description": "" }, "textEdit": { "range": { "start": { "line": 2, "character": 17 }, "end": { "line": 2, "character": 18 }, }, - "newText": "LICENSE" + "newText": "command.nu" }, "kind": 17 }) @@ -337,10 +337,10 @@ mod tests { let resp = send_complete_request(&client_connection, script, 5, 4); assert!(result_from_message(resp).as_array().unwrap().contains( &serde_json::json!({ - "label": "LICENSE", + "label": "cell_path.nu", "labelDetails": { "description": "" }, "textEdit": { "range": { "start": { "line": 5, "character": 3 }, "end": { "line": 5, "character": 4 }, }, - "newText": "LICENSE" + "newText": "cell_path.nu" }, "kind": 17 }) diff --git a/crates/nu-lsp/src/diagnostics.rs b/crates/nu-lsp/src/diagnostics.rs index 2c41b7fde6..d6fcc58cd6 100644 --- a/crates/nu-lsp/src/diagnostics.rs +++ b/crates/nu-lsp/src/diagnostics.rs @@ -7,7 +7,7 @@ use miette::{miette, IntoDiagnostic, Result}; impl LanguageServer { pub(crate) fn publish_diagnostics_for_file(&mut self, uri: Uri) -> Result<()> { - let mut engine_state = self.new_engine_state(); + let mut engine_state = self.new_engine_state(Some(&uri)); engine_state.generate_nu_constant(); let Some((_, span, working_set)) = self.parse_file(&mut engine_state, &uri, true) else { diff --git a/crates/nu-lsp/src/goto.rs b/crates/nu-lsp/src/goto.rs index c61d2f1485..8572196fe4 100644 --- a/crates/nu-lsp/src/goto.rs +++ b/crates/nu-lsp/src/goto.rs @@ -77,13 +77,12 @@ impl LanguageServer { &mut self, params: &GotoDefinitionParams, ) -> Option { - let mut engine_state = self.new_engine_state(); - let path_uri = params .text_document_position_params .text_document .uri .to_owned(); + let mut engine_state = self.new_engine_state(Some(&path_uri)); let (working_set, id, _, _) = self .parse_and_find( &mut engine_state, diff --git a/crates/nu-lsp/src/hover.rs b/crates/nu-lsp/src/hover.rs index 8072e1b68d..4971ada75a 100644 --- a/crates/nu-lsp/src/hover.rs +++ b/crates/nu-lsp/src/hover.rs @@ -129,13 +129,12 @@ impl LanguageServer { } pub(crate) fn hover(&mut self, params: &HoverParams) -> Option { - let mut engine_state = self.new_engine_state(); - let path_uri = params .text_document_position_params .text_document .uri .to_owned(); + let mut engine_state = self.new_engine_state(Some(&path_uri)); let (working_set, id, _, _) = self .parse_and_find( &mut engine_state, diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index d48e425c43..111a736b8c 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -13,7 +13,7 @@ use miette::{miette, IntoDiagnostic, Result}; use nu_protocol::{ ast::{Block, PathMember}, engine::{EngineState, StateDelta, StateWorkingSet}, - DeclId, ModuleId, Span, Type, VarId, + DeclId, ModuleId, Span, Type, Value, VarId, }; use std::{ collections::BTreeMap, @@ -315,13 +315,26 @@ impl LanguageServer { Ok(reset) } - pub(crate) fn new_engine_state(&self) -> EngineState { + /// Create a clone of the initial_engine_state with: + /// + /// * PWD set to the parent directory of given uri. Fallback to `$env.PWD` if None. + /// * `StateDelta` cache merged + pub(crate) fn new_engine_state(&self, uri: Option<&Uri>) -> EngineState { let mut engine_state = self.initial_engine_state.clone(); - let cwd = std::env::current_dir().expect("Could not get current working directory."); - engine_state.add_env_var( - "PWD".into(), - nu_protocol::Value::test_string(cwd.to_string_lossy()), - ); + match uri { + Some(uri) => { + let path = uri_to_path(uri); + if let Some(path) = path.parent() { + engine_state + .add_env_var("PWD".into(), Value::test_string(path.to_string_lossy())) + }; + } + None => { + let cwd = + std::env::current_dir().expect("Could not get current working directory."); + engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy())); + } + } // merge the cached `StateDelta` if text not changed if !self.need_parse { engine_state @@ -350,7 +363,7 @@ impl LanguageServer { engine_state: &'a mut EngineState, uri: &Uri, pos: Position, - ) -> Result<(StateWorkingSet<'a>, Id, Span, usize)> { + ) -> Result<(StateWorkingSet<'a>, Id, Span, Span)> { let (block, file_span, working_set) = self .parse_file(engine_state, uri, false) .ok_or_else(|| miette!("\nFailed to parse current file"))?; @@ -365,7 +378,7 @@ impl LanguageServer { let location = file.offset_at(pos) as usize + file_span.start; let (id, span) = ast::find_id(&block, &working_set, &location) .ok_or_else(|| miette!("\nFailed to find current name"))?; - Ok((working_set, id, span, file_span.start)) + Ok((working_set, id, span, file_span)) } pub(crate) fn parse_file<'a>( @@ -458,10 +471,7 @@ mod tests { 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(), - nu_protocol::Value::test_string(cwd.to_string_lossy()), - ); + engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy())); if let Some(code) = nu_config_code { assert!(merge_input(code.as_bytes(), &mut engine_state, &mut Stack::new()).is_ok()); } diff --git a/crates/nu-lsp/src/signature.rs b/crates/nu-lsp/src/signature.rs index aca08477e2..d431531d8c 100644 --- a/crates/nu-lsp/src/signature.rs +++ b/crates/nu-lsp/src/signature.rs @@ -78,7 +78,7 @@ impl LanguageServer { let file_text = file.get_content(None).to_owned(); drop(docs); - let engine_state = self.new_engine_state(); + let engine_state = self.new_engine_state(Some(&path_uri)); let mut working_set = StateWorkingSet::new(&engine_state); // NOTE: in case the cursor is at the end of the call expression diff --git a/crates/nu-lsp/src/symbols.rs b/crates/nu-lsp/src/symbols.rs index 4d2cf67dc6..0a02d383a8 100644 --- a/crates/nu-lsp/src/symbols.rs +++ b/crates/nu-lsp/src/symbols.rs @@ -270,8 +270,8 @@ impl LanguageServer { &mut self, params: &DocumentSymbolParams, ) -> Option { - let engine_state = self.new_engine_state(); let uri = params.text_document.uri.to_owned(); + let engine_state = self.new_engine_state(Some(&uri)); let docs = self.docs.lock().ok()?; self.symbol_cache.update(&uri, &engine_state, &docs); self.symbol_cache @@ -284,7 +284,7 @@ impl LanguageServer { params: &WorkspaceSymbolParams, ) -> Option { if self.symbol_cache.any_dirty() { - let engine_state = self.new_engine_state(); + let engine_state = self.new_engine_state(None); let docs = self.docs.lock().ok()?; self.symbol_cache.update_all(&engine_state, &docs); } diff --git a/crates/nu-lsp/src/workspace.rs b/crates/nu-lsp/src/workspace.rs index 3818834829..8036af5c5e 100644 --- a/crates/nu-lsp/src/workspace.rs +++ b/crates/nu-lsp/src/workspace.rs @@ -1,5 +1,5 @@ use crate::{ - ast::{find_id, find_reference_by_id}, + ast::{self, find_id, find_reference_by_id}, path_to_uri, span_to_range, uri_to_path, Id, LanguageServer, }; use lsp_textdocument::FullTextDocument; @@ -46,6 +46,26 @@ fn find_nu_scripts_in_folder(folder_uri: &Uri) -> Result { nu_glob::glob(&pattern, Uninterruptible).into_diagnostic() } +/// HACK: when current file is imported (use keyword) by others in the workspace, +/// it will get parsed a second time via `parse_module_block`, so that its definitions' +/// ids are renewed, making it harder to track the references. +/// +/// FIXME: cross-file shadowing can still cause false-positive/false-negative cases +/// +/// This is a workaround to track the new id +struct IDTracker { + /// ID to search, renewed on `parse_module_block` + pub id: Id, + /// Span of the original instance under the cursor + pub span: Span, + /// Name of the definition + pub name: String, + /// Span of the original file where the request comes from + pub file_span: Span, + /// The redundant parsing should only happen once + pub renewed: bool, +} + impl LanguageServer { /// Get initial workspace folders from initialization response pub(crate) fn initialize_workspace_folders( @@ -66,12 +86,12 @@ impl LanguageServer { &mut self, params: &DocumentHighlightParams, ) -> Option> { - let mut engine_state = self.new_engine_state(); let path_uri = params .text_document_position_params .text_document .uri .to_owned(); + let mut engine_state = self.new_engine_state(Some(&path_uri)); let (block, file_span, working_set) = self.parse_file(&mut engine_state, &path_uri, false)?; let docs = &self.docs.lock().ok()?; @@ -137,31 +157,38 @@ impl LanguageServer { timeout: u128, ) -> Option> { self.occurrences = BTreeMap::new(); - let mut engine_state = self.new_engine_state(); let path_uri = params.text_document_position.text_document.uri.to_owned(); - let (_, id, span, _) = self + let mut engine_state = self.new_engine_state(Some(&path_uri)); + + let (working_set, id, span, file_span) = self .parse_and_find( &mut engine_state, &path_uri, params.text_document_position.position, ) .ok()?; - // have to clone it again in order to move to another thread - let engine_state = self.new_engine_state(); let current_workspace_folder = self.get_workspace_folder_by_uri(&path_uri)?; let token = params .work_done_progress_params .work_done_token .to_owned() .unwrap_or(ProgressToken::Number(1)); + + let id_tracker = IDTracker { + id, + span, + file_span, + name: String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(), + renewed: false, + }; + self.channels = self .find_reference_in_workspace( engine_state, current_workspace_folder, - id, - span, token.clone(), "Finding references ...".to_string(), + id_tracker, ) .ok(); // TODO: WorkDoneProgress -> PartialResults for quicker response @@ -200,10 +227,10 @@ impl LanguageServer { serde_json::from_value(request.params).into_diagnostic()?; self.occurrences = BTreeMap::new(); - let mut engine_state = self.new_engine_state(); let path_uri = params.text_document.uri.to_owned(); + let mut engine_state = self.new_engine_state(Some(&path_uri)); - let (working_set, id, span, file_offset) = + let (working_set, id, span, file_span) = self.parse_and_find(&mut engine_state, &path_uri, params.position)?; if let Id::Value(_) = id { @@ -222,7 +249,7 @@ impl LanguageServer { let file = docs .get_document(&path_uri) .ok_or_else(|| miette!("\nFailed to get document"))?; - let range = span_to_range(&span, file, file_offset); + let range = span_to_range(&span, file, file_span.start); let response = PrepareRenameResponse::Range(range); self.connection .sender @@ -233,20 +260,24 @@ impl LanguageServer { })) .into_diagnostic()?; - // have to clone it again in order to move to another thread - let engine_state = self.new_engine_state(); let current_workspace_folder = self .get_workspace_folder_by_uri(&path_uri) .ok_or_else(|| miette!("\nCurrent file is not in any workspace"))?; // now continue parsing on other files in the workspace + let id_tracker = IDTracker { + id, + span, + file_span, + name: String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(), + renewed: false, + }; self.channels = self .find_reference_in_workspace( engine_state, current_workspace_folder, - id, - span, ProgressToken::Number(0), "Preparing rename ...".to_string(), + id_tracker, ) .ok(); Ok(()) @@ -256,7 +287,7 @@ impl LanguageServer { working_set: &mut StateWorkingSet, file: &FullTextDocument, fp: &Path, - id: &Id, + id_tracker: &mut IDTracker, ) -> Option> { let block = nu_parser::parse( working_set, @@ -264,7 +295,25 @@ impl LanguageServer { file.get_content(None).as_bytes(), false, ); - let references: Vec = find_reference_by_id(&block, working_set, id); + // NOTE: Renew the id if there's a module with the same span as the original file. + // This requires that the initial parsing results get merged in the engine_state, + // typically they're cached with diagnostics before the prepare_rename/references requests, + // so that we don't need to clone and merge delta again. + if (!id_tracker.renewed) + && working_set + .find_module_by_span(id_tracker.file_span) + .is_some() + { + if let Some(new_block) = working_set.find_block_by_span(id_tracker.file_span) { + if let Some((new_id, _)) = + ast::find_id(&new_block, working_set, &id_tracker.span.start) + { + id_tracker.id = new_id; + } + } + id_tracker.renewed = true; + } + let references: Vec = find_reference_by_id(&block, working_set, &id_tracker.id); // add_block to avoid repeated parsing working_set.add_block(block); @@ -304,10 +353,9 @@ impl LanguageServer { &self, engine_state: EngineState, current_workspace_folder: WorkspaceFolder, - id: Id, - span: Span, token: ProgressToken, message: String, + mut id_tracker: IDTracker, ) -> Result<( crossbeam_channel::Sender, Arc>, @@ -333,7 +381,7 @@ impl LanguageServer { .filter_map(|p| p.ok()) .collect(); let len = scripts.len(); - let definition_span = Self::find_definition_span_by_id(&working_set, &id); + let definition_span = Self::find_definition_span_by_id(&working_set, &id_tracker.id); for (i, fp) in scripts.iter().enumerate() { #[cfg(test)] @@ -363,9 +411,7 @@ impl LanguageServer { }; // skip if the file does not contain what we're looking for let content_string = String::from_utf8_lossy(&bytes); - let text_to_search = - String::from_utf8_lossy(working_set.get_span_contents(span)); - if !content_string.contains(text_to_search.as_ref()) { + if !content_string.contains(&id_tracker.name) { // progress without any data data_sender .send(InternalMessage::OnGoing(token.clone(), percentage)) @@ -374,17 +420,17 @@ impl LanguageServer { } &FullTextDocument::new("nu".to_string(), 0, content_string.into()) }; - let _ = Self::find_reference_in_file(&mut working_set, file, fp, &id).map( - |mut refs| { + let _ = Self::find_reference_in_file(&mut working_set, file, fp, &mut id_tracker) + .map(|mut refs| { let file_span = working_set .get_span_for_filename(fp.to_string_lossy().as_ref()) .unwrap_or(Span::unknown()); if let Some(extra_span) = Self::reference_not_in_ast( - &id, + &id_tracker.id, &working_set, definition_span, file_span, - span, + id_tracker.span, ) { if !refs.contains(&extra_span) { refs.push(extra_span) @@ -400,8 +446,7 @@ impl LanguageServer { data_sender .send(InternalMessage::OnGoing(token.clone(), percentage)) .ok(); - }, - ); + }); } data_sender .send(InternalMessage::Finished(token.clone())) diff --git a/tests/fixtures/lsp/completion/command.nu b/tests/fixtures/lsp/completion/command.nu index cf30603ccf..ca2bd5cc51 100644 --- a/tests/fixtures/lsp/completion/command.nu +++ b/tests/fixtures/lsp/completion/command.nu @@ -1,6 +1,6 @@ config n config n foo bar - -config n foo bar l --l +config n foo bar c --l # detail def "config n foo bar" [ diff --git a/tests/fixtures/lsp/completion/fallback.nu b/tests/fixtures/lsp/completion/fallback.nu index 4e454cd552..1aa5edcbd6 100644 --- a/tests/fixtures/lsp/completion/fallback.nu +++ b/tests/fixtures/lsp/completion/fallback.nu @@ -3,6 +3,6 @@ let greeting = "Hello" echo $gre | st -ls l +ls c $greeting not-h From a72f94f452b2ef78fe33fb5c9ea76f27133810fe Mon Sep 17 00:00:00 2001 From: zc he Date: Sat, 5 Apr 2025 22:23:27 +0800 Subject: [PATCH 03/13] feat(lsp): snippet style completion for commands (#15494) # Description For example: here's what happens after selecting the `if` command from the completion menu: image image Missing arguments are inserted as placeholders in a snippet, just as function name completions in other lsp servers like rust-analyzer and clangd. # User-Facing Changes Press tab to navigate Flags still need to be added manually # Tests + Formatting Refined # After Submitting --- crates/nu-lsp/src/completion.rs | 118 ++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/crates/nu-lsp/src/completion.rs b/crates/nu-lsp/src/completion.rs index e7573857f9..a3650c5f32 100644 --- a/crates/nu-lsp/src/completion.rs +++ b/crates/nu-lsp/src/completion.rs @@ -3,12 +3,13 @@ use std::sync::Arc; use crate::{span_to_range, uri_to_path, LanguageServer}; use lsp_types::{ CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, - CompletionResponse, CompletionTextEdit, Documentation, MarkupContent, MarkupKind, TextEdit, + CompletionResponse, CompletionTextEdit, Documentation, InsertTextFormat, MarkupContent, + MarkupKind, TextEdit, }; use nu_cli::{NuCompleter, SuggestionKind}; use nu_protocol::{ engine::{CommandType, Stack}, - Span, + PositionalArg, Span, SyntaxShape, }; impl LanguageServer { @@ -45,27 +46,71 @@ impl LanguageServer { results .into_iter() .map(|r| { - let decl_id = r.kind.clone().and_then(|kind| { + let decl_id = r.kind.as_ref().and_then(|kind| { matches!(kind, SuggestionKind::Command(_)) .then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?) }); - let mut label_value = r.suggestion.value; - if r.suggestion.append_whitespace { - label_value.push(' '); + let mut snippet_text = r.suggestion.value.clone(); + let mut doc_string = r.suggestion.extra.map(|ex| ex.join("\n")); + let mut insert_text_format = None; + let mut idx = 1; + // use snippet as `insert_text_format` for command argument completion + if let Some(decl_id) = decl_id { + let cmd = engine_state.get_decl(decl_id); + doc_string = Some(Self::get_decl_description(cmd, true)); + insert_text_format = Some(InsertTextFormat::SNIPPET); + let signature = cmd.signature(); + // add curly brackets around block arguments + let block_wrapper = |arg: &PositionalArg, text: String| -> String { + if matches!(arg.shape, SyntaxShape::Block | SyntaxShape::MatchBlock) { + format!("{{ {text} }}") + } else { + text + } + }; + + for required in signature.required_positional { + snippet_text.push(' '); + snippet_text.push_str( + block_wrapper(&required, format!("${{{}:{}}}", idx, required.name)) + .as_str(), + ); + idx += 1; + } + for optional in signature.optional_positional { + snippet_text.push(' '); + snippet_text.push_str( + block_wrapper( + &optional, + format!("${{{}:{}?}}", idx, optional.name), + ) + .as_str(), + ); + idx += 1; + } + if let Some(rest) = signature.rest_positional { + snippet_text + .push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str()); + idx += 1; + } + } + // no extra space for a command with args expanded in the snippet + if idx == 1 && r.suggestion.append_whitespace { + snippet_text.push(' '); } let span = r.suggestion.span; let text_edit = Some(CompletionTextEdit::Edit(TextEdit { range: span_to_range(&Span::new(span.start, span.end), file, 0), - new_text: label_value.clone(), + new_text: snippet_text, })); CompletionItem { - label: label_value, + label: r.suggestion.value, label_details: r .kind - .clone() + .as_ref() .map(|kind| match kind { SuggestionKind::Value(t) => t.to_string(), SuggestionKind::Command(cmd) => cmd.to_string(), @@ -80,21 +125,15 @@ impl LanguageServer { description: Some(s), }), detail: r.suggestion.description, - documentation: r - .suggestion - .extra - .map(|ex| ex.join("\n")) - .or(decl_id.map(|decl_id| { - Self::get_decl_description(engine_state.get_decl(decl_id), true) - })) - .map(|value| { - Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value, - }) - }), + documentation: doc_string.map(|value| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value, + }) + }), kind: Self::lsp_completion_item_kind(r.kind), text_edit, + insert_text_format, ..Default::default() } }) @@ -221,9 +260,9 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ // defined after the cursor - { "label": "config n foo bar ", "detail": detail_str, "kind": 2 }, + { "label": "config n foo bar", "detail": detail_str, "kind": 2 }, { - "label": "config nu ", + "label": "config nu", "detail": "Edit nu configurations.", "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, }, "newText": "config nu " @@ -236,7 +275,7 @@ mod tests { let resp = send_complete_request(&client_connection, script.clone(), 1, 18); assert!(result_from_message(resp).as_array().unwrap().contains( &serde_json::json!({ - "label": "-s ", + "label": "-s", "detail": "test flag", "labelDetails": { "description": "flag" }, "textEdit": { "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 18 }, }, @@ -250,7 +289,7 @@ mod tests { let resp = send_complete_request(&client_connection, script.clone(), 2, 22); assert!(result_from_message(resp).as_array().unwrap().contains( &serde_json::json!({ - "label": "--long ", + "label": "--long", "detail": "test flag", "labelDetails": { "description": "flag" }, "textEdit": { "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 22 }, }, @@ -277,7 +316,7 @@ mod tests { let resp = send_complete_request(&client_connection, script, 10, 34); assert!(result_from_message(resp).as_array().unwrap().contains( &serde_json::json!({ - "label": "-g ", + "label": "-g", "detail": "count indexes and split using grapheme clusters (all visible chars have length 1)", "labelDetails": { "description": "flag" }, "textEdit": { "range": { "start": { "line": 10, "character": 33 }, "end": { "line": 10, "character": 34 }, }, @@ -305,13 +344,14 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "alias ", + "label": "alias", "labelDetails": { "description": "keyword" }, "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, - "newText": "alias " + "newText": "alias ${1:name} ${2:initial_value}" }, + "insertTextFormat": 2, "kind": 14 } ]) @@ -322,13 +362,14 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "alias ", + "label": "alias", "labelDetails": { "description": "keyword" }, "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 2 }, }, - "newText": "alias " + "newText": "alias ${1:name} ${2:initial_value}" }, + "insertTextFormat": 2, "kind": 14 } ]) @@ -364,13 +405,14 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "str trim ", + "label": "str trim", "labelDetails": { "description": "built-in" }, "detail": "Trim whitespace or specific character.", "textEdit": { "range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, }, - "newText": "str trim " + "newText": "str trim ${1:...rest}" }, + "insertTextFormat": 2, "kind": 3 } ]) @@ -394,7 +436,7 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "overlay ", + "label": "overlay", "labelDetails": { "description": "keyword" }, "textEdit": { "newText": "overlay ", @@ -483,12 +525,12 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "alias ", + "label": "alias", "labelDetails": { "description": "keyword" }, "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 5 }, }, - "newText": "alias " + "newText": "alias ${1:name} ${2:initial_value}" }, "kind": 14 }, @@ -513,7 +555,7 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "!= ", + "label": "!=", "labelDetails": { "description": "operator" }, "textEdit": { "newText": "!= ", @@ -529,7 +571,7 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!([ { - "label": "not-has ", + "label": "not-has", "labelDetails": { "description": "operator" }, "textEdit": { "newText": "not-has ", From f25525be6cc0343d77f232f8ccecde25798d56aa Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Sat, 5 Apr 2025 09:24:16 -0500 Subject: [PATCH 04/13] Revert "Fix #15394 for `table -e` wrapping issue" (#15498) Reverts nushell/nushell#15407 Reopens https://github.com/nushell/nushell/issues/15394 @zhiburt Reverting due to some strange coloring I didn't notice before. Notice the last row. This is the command that produced this table `help commands | group-by command_type | get external` ![image](https://github.com/user-attachments/assets/ea2d14e3-0efd-4ef2-a3a9-bccbf41a3eae) This is what it looks like after the revert. Notice the column header colors. Wrapping is also a little bit different even though my terminal size didn't change. Notice `search_terms` was kind of eaten above. ![image](https://github.com/user-attachments/assets/526eb8e2-eb87-4aeb-89c1-b88f65354368) --- crates/nu-command/tests/commands/table.rs | 386 ++++------------ crates/nu-table/src/table.rs | 512 ++++++++++++++-------- crates/nu-table/src/types/expanded.rs | 11 +- crates/nu-table/src/util.rs | 21 - typos.toml | 1 - 5 files changed, 408 insertions(+), 523 deletions(-) diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 6bd7dfbc00..cc40a41a74 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -1333,15 +1333,7 @@ fn test_expand_big_0() { "│ target │ {record 3 fields} │", "│ dev-dependencies │ {record 9 fields} │", "│ features │ {record 8 fields} │", - "│ │ ╭───┬─────┬─────╮ │", - "│ bin │ │ # │ nam │ pat │ │", - "│ │ │ │ e │ h │ │", - "│ │ ├───┼─────┼─────┤ │", - "│ │ │ 0 │ nu │ src │ │", - "│ │ │ │ │ /ma │ │", - "│ │ │ │ │ in. │ │", - "│ │ │ │ │ rs │ │", - "│ │ ╰───┴─────┴─────╯ │", + "│ bin │ [table 1 row] │", "│ │ ╭───────────┬───╮ │", "│ patch │ │ crates-io │ { │ │", "│ │ │ │ r │ │", @@ -1360,16 +1352,7 @@ fn test_expand_big_0() { "│ │ │ │ d │ │", "│ │ │ │ } │ │", "│ │ ╰───────────┴───╯ │", - "│ │ ╭───┬─────┬─────╮ │", - "│ bench │ │ # │ nam │ har │ │", - "│ │ │ │ e │ nes │ │", - "│ │ │ │ │ s │ │", - "│ │ ├───┼─────┼─────┤ │", - "│ │ │ 0 │ ben │ fal │ │", - "│ │ │ │ chm │ se │ │", - "│ │ │ │ ark │ │ │", - "│ │ │ │ s │ │ │", - "│ │ ╰───┴─────┴─────╯ │", + "│ bench │ [table 1 row] │", "╰──────────────────┴───────────────────╯", ]); @@ -1383,8 +1366,6 @@ fn table_expande_with_no_header_internally_0() { let actual = nu!(format!("{} | table --expand --width 141", nu_value.trim())); - _print_lines(&actual.out, 141); - assert_eq!( actual.out, join_lines([ @@ -1551,191 +1532,71 @@ fn table_expande_with_no_header_internally_0() { "│ │ │ │ │ │ ╰─────┴──────────╯ │ │ │", "│ │ │ │ │ display_output │ │ │ │", "│ │ │ │ ╰────────────────┴────────────────────╯ │ │", - "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬───┬─────╮ │ │", - "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ t │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ y │ │ │ │", - "│ │ │ │ │ │ │ │ │ p │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼───┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ false │ | │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 4 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ │ 1 │ history_menu │ true │ ? │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 2 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ │ 2 │ help_menu │ true │ ? │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 6 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ │ 3 │ commands_menu │ false │ # │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 4 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ │ 4 │ vars_menu │ true │ # │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 2 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ { │ ... │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ c │ │ │ │", - "│ │ │ │ │ │ │ │ │ o │ │ │ │", - "│ │ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ 6 │ │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ │ │", - "│ │ │ │ │ │ │ │ │ f │ │ │ │", - "│ │ │ │ │ │ │ │ │ i │ │ │ │", - "│ │ │ │ │ │ │ │ │ e │ │ │ │", - "│ │ │ │ │ │ │ │ │ l │ │ │ │", - "│ │ │ │ │ │ │ │ │ d │ │ │ │", - "│ │ │ │ │ │ │ │ │ s │ │ │ │", - "│ │ │ │ │ │ │ │ │ } │ │ │ │", - "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴───┴─────╯ │ │", + "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬────────┬─────╮ │ │", + "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marker │ ... │ │ │", + "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼────────┼─────┤ │ │", + "│ │ │ │ │ 0 │ completion_menu │ false │ | │ ... │ │ │", + "│ │ │ │ │ 1 │ history_menu │ true │ ? │ ... │ │ │", + "│ │ │ │ │ 2 │ help_menu │ true │ ? │ ... │ │ │", + "│ │ │ │ │ 3 │ commands_menu │ false │ # │ ... │ │ │", + "│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │", + "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │", + "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴────────┴─────╯ │ │", "│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬───────────────┬─────╮ │ │", - "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │", - "│ │ │ │ │ │ │ │ │ │ nt │ │ │", + "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ... │ │ │", "│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼───────────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", + "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", + "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ s} │ │ │", - "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", + "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ ... │ │ │", + "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ ... │ │ │", + "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ ... │ │ │", + "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ ... │ │ │", + "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ } │ │ │", + "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", + "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", + "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", - "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───────╮ │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ │ sert │ │ s} │ │ │", + "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ ╭───┬───────╮ │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ │ 0 │ emacs │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 1 │ vi_no │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ rmal │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ 2 │ vi_in │ │ │ │ │", + "│ │ │ │ │ │ │ │ │ │ │ sert │ │ │ │ │", "│ │ │ │ │ │ │ │ │ ╰───┴───────╯ │ │ │ │", "│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴───────────────┴─────╯ │ │", "│ │ ╰──────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────╯ │", @@ -1750,8 +1611,6 @@ fn table_expande_with_no_header_internally_1() { let actual = nu!(format!("{} | table --expand --width 136", nu_value.trim())); - _print_lines(&actual.out, 136); - assert_eq!( actual.out, join_lines([ @@ -1918,87 +1777,37 @@ fn table_expande_with_no_header_internally_1() { "│ │ │ │ │ │ ╰─────┴──────────╯ │ │ │", "│ │ │ │ │ display_output │ │ │ │", "│ │ │ │ ╰────────────────┴────────────────────╯ │ │", - "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬───────┬─────╮ │ │", - "│ │ │ menus │ │ # │ name │ only_buffer_difference │ marke │ ... │ │ │", - "│ │ │ │ │ │ │ │ r │ │ │ │", - "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼───────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ false │ | │ ... │ │ │", - "│ │ │ │ │ 1 │ history_menu │ true │ ? │ ... │ │ │", - "│ │ │ │ │ 2 │ help_menu │ true │ ? │ ... │ │ │", - "│ │ │ │ │ 3 │ commands_menu │ false │ # │ ... │ │ │", - "│ │ │ │ │ 4 │ vars_menu │ true │ # │ ... │ │ │", - "│ │ │ │ │ 5 │ commands_with_description │ true │ # │ ... │ │ │", - "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴───────┴─────╯ │ │", + "│ │ │ │ ╭───┬───────────────────────────┬────────────────────────┬─────╮ │ │", + "│ │ │ menus │ │ # │ name │ only_buffer_difference │ ... │ │ │", + "│ │ │ │ ├───┼───────────────────────────┼────────────────────────┼─────┤ │ │", + "│ │ │ │ │ 0 │ completion_menu │ false │ ... │ │ │", + "│ │ │ │ │ 1 │ history_menu │ true │ ... │ │ │", + "│ │ │ │ │ 2 │ help_menu │ true │ ... │ │ │", + "│ │ │ │ │ 3 │ commands_menu │ false │ ... │ │ │", + "│ │ │ │ │ 4 │ vars_menu │ true │ ... │ │ │", + "│ │ │ │ │ 5 │ commands_with_description │ true │ ... │ │ │", + "│ │ │ │ ╰───┴───────────────────────────┴────────────────────────┴─────╯ │ │", "│ │ │ │ ╭────┬───────────────────────────┬──────────┬─────────┬──────────┬─────╮ │ │", - "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ eve │ │ │", - "│ │ │ │ │ │ │ │ │ │ nt │ │ │", + "│ │ │ keybindings │ │ # │ name │ modifier │ keycode │ mode │ ... │ │ │", "│ │ │ │ ├────┼───────────────────────────┼──────────┼─────────┼──────────┼─────┤ │ │", - "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ s} │ │ │", - "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 1 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ } │ │ │", - "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ s} │ │ │", - "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ s} │ │ │", - "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ [list 3 │ {re │ │ │", - "│ │ │ │ │ │ │ │ │ items] │ cor │ │ │", - "│ │ │ │ │ │ │ │ │ │ d 2 │ │ │", - "│ │ │ │ │ │ │ │ │ │ fi │ │ │", - "│ │ │ │ │ │ │ │ │ │ eld │ │ │", - "│ │ │ │ │ │ │ │ │ │ s} │ │ │", + "│ │ │ │ │ 0 │ completion_menu │ none │ tab │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 1 │ completion_previous │ shift │ backtab │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 2 │ history_menu │ control │ char_r │ emacs │ ... │ │ │", + "│ │ │ │ │ 3 │ next_page │ control │ char_x │ emacs │ ... │ │ │", + "│ │ │ │ │ 4 │ undo_or_previous_page │ control │ char_z │ emacs │ ... │ │ │", + "│ │ │ │ │ 5 │ yank │ control │ char_y │ emacs │ ... │ │ │", + "│ │ │ │ │ 6 │ unix-line-discard │ control │ char_u │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 7 │ kill-line │ control │ char_k │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 8 │ commands_menu │ control │ char_t │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 9 │ vars_menu │ alt │ char_o │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", + "│ │ │ │ │ 10 │ commands_with_description │ control │ char_s │ [list 3 │ ... │ │ │", + "│ │ │ │ │ │ │ │ │ items] │ │ │ │", "│ │ │ │ ╰────┴───────────────────────────┴──────────┴─────────┴──────────┴─────╯ │ │", "│ │ ╰──────────────────────────────────┴──────────────────────────────────────────────────────────────────────────╯ │", "╰────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", @@ -2712,7 +2521,6 @@ fn table_theme_on_border_with_love() { fn table_theme_on_border_thin() { assert_eq!( create_theme_output("thin"), - // ["┌─#─┬a_looooooong_name┬─b─┬─c─┐│ 0 │ 1 │ 2 │ 3 │└─#─┴a_looooooong_name┴─b─┴─c─┘"] [ "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└───┴───┴───┴────────────────┘", "┌─#─┬─a─┬─b─┬───────c────────┐│ 0 │ 1 │ 2 │ 3 │├───┼───┼───┼────────────────┤│ 1 │ 4 │ 5 │ [list 3 items] │└─#─┴─a─┴─b─┴───────c────────┘", @@ -3341,21 +3149,3 @@ fn table_index_expand() { ╰─────┴─────╯" ); } - -#[test] -fn table_expand_big_header() { - let actual = nu!(" - let column_name = (('' | fill -c 'a' --width 81)) - [{ $column_name: 'contents' }] | table -e --width=80 - "); - - assert_eq!( - actual.out, - "╭───┬──────────────────────────────────────────────────────────────────────────╮\ - │ # │ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │\ - │ │ aaaaaaaaa │\ - ├───┼──────────────────────────────────────────────────────────────────────────┤\ - │ 0 │ contents │\ - ╰───┴──────────────────────────────────────────────────────────────────────────╯" - ); -} diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 90d23e0269..b706a41eb2 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -3,30 +3,33 @@ use std::{cmp::min, collections::HashMap}; use nu_ansi_term::Style; use nu_color_config::TextStyle; use nu_protocol::{TableIndent, TrimStrategy}; +use nu_utils::strip_ansi_unlikely; use tabled::{ builder::Builder, grid::{ ansi::ANSIBuf, + colors::Colors, config::{AlignmentHorizontal, ColoredConfig, Entity, Position}, dimension::CompleteDimensionVecRecords, records::{ vec_records::{Cell, Text, VecRecords}, - ExactRecords, Records, + ExactRecords, Records, Resizable, }, }, settings::{ + format::FormatContent, formatting::AlignmentStrategy, - object::{Columns, Rows}, + object::{Columns, Row, Rows}, peaker::Priority, themes::ColumnNames, width::Truncate, - Alignment, Color, Modify, Padding, TableOption, Width, + Alignment, Color, Format, Modify, ModifyList, Padding, Settings, TableOption, Width, }, Table, }; -use crate::{convert_style, is_color_empty, string_width, table_theme::TableTheme}; +use crate::{convert_style, is_color_empty, table_theme::TableTheme}; pub type NuRecords = VecRecords; pub type NuRecordsValue = Text; @@ -241,41 +244,14 @@ impl TableStructure { } } -#[derive(Debug, Clone)] -struct HeadInfo { - values: Vec, - align: AlignmentHorizontal, - color: Option, -} - fn build_table(mut t: NuTable, termwidth: usize) -> Option { if t.count_columns() == 0 || t.count_rows() == 0 { return Some(String::new()); } - let mut head = None; - if is_header_on_border(&t) { - head = Some(remove_header(&mut t)); - } else { - table_insert_footer(&mut t); - } - - let widths = table_truncate(&mut t, head.clone(), termwidth)?; - if let Some(head) = head.as_mut() { - if head.values.len() > widths.len() { - head.values[widths.len() - 1] = String::from("..."); - } - } - - draw_table(t, widths, head, termwidth) -} - -fn is_header_on_border(t: &NuTable) -> bool { - let structure = get_table_structure(&t.data, &t.config); - let is_configured = structure.with_header && t.config.header_on_border; - let has_horizontal = t.config.theme.as_base().borders_has_top() - || t.config.theme.as_base().get_horizontal_line(1).is_some(); - is_configured && has_horizontal + let widths = table_truncate(&mut t, termwidth)?; + table_insert_footer(&mut t); + draw_table(t, widths, termwidth) } fn table_insert_footer(t: &mut NuTable) { @@ -284,8 +260,9 @@ fn table_insert_footer(t: &mut NuTable) { } } -fn table_truncate(t: &mut NuTable, head: Option, termwidth: usize) -> Option> { - let widths = maybe_truncate_columns(&mut t.data, &t.config, head, termwidth); +fn table_truncate(t: &mut NuTable, termwidth: usize) -> Option> { + let pad = t.config.indent.left + t.config.indent.right; + let widths = maybe_truncate_columns(&mut t.data, &t.config, termwidth, pad); if widths.is_empty() { return None; } @@ -293,52 +270,10 @@ fn table_truncate(t: &mut NuTable, head: Option, termwidth: usize) -> Some(widths) } -fn remove_header(t: &mut NuTable) -> HeadInfo { - let head: Vec = t - .data - .remove(0) - .into_iter() - .map(|s| s.to_string()) - .collect(); - let align = t.alignments.header; - let color = is_color_empty(&t.styles.header).then(|| t.styles.header.clone()); - - // move settings by one row down - t.alignments.cells = t - .alignments - .cells - .drain() - .filter(|(k, _)| k.0 != 0) - .map(|(k, v)| ((k.0 - 1, k.1), v)) - .collect(); - - // move settings by one row down - t.styles.cells = t - .styles - .cells - .drain() - .filter(|(k, _)| k.0 != 0) - .map(|(k, v)| ((k.0 - 1, k.1), v)) - .collect(); - - HeadInfo { - values: head, - align, - color, - } -} - -fn draw_table( - t: NuTable, - widths: Vec, - head: Option, - termwidth: usize, -) -> Option { - let mut structure = get_table_structure(&t.data, &t.config); +fn draw_table(t: NuTable, widths: Vec, termwidth: usize) -> Option { + let structure = get_table_structure(&t.data, &t.config); let sep_color = t.config.border_color; - if head.is_some() { - structure.with_header = false; - } + let border_header = structure.with_header && t.config.header_on_border; let data: Vec> = t.data.into(); let mut table = Builder::from_vec(data).build(); @@ -347,51 +282,15 @@ fn draw_table( load_theme(&mut table, &t.config.theme, &structure, sep_color); align_table(&mut table, t.alignments, &structure); colorize_table(&mut table, t.styles, &structure); - truncate_table(&mut table, t.config.clone(), widths, termwidth); - table_set_border_header(&mut table, head, &t.config.theme, structure); + + let pad = indent_sum(t.config.indent); + let width_ctrl = WidthCtrl::new(widths, t.config, termwidth, pad); + + adjust_table(&mut table, width_ctrl, border_header, structure.with_footer); table_to_string(table, termwidth) } -fn table_set_border_header( - table: &mut Table, - head: Option, - theme: &TableTheme, - structure: TableStructure, -) { - let head = match head { - Some(head) => head, - None => return, - }; - - let mut widths = GetDims(Vec::new()); - table.with(&mut widths); - - if !theme.as_base().borders_has_top() { - let line = theme.as_base().get_horizontal_line(1); - if let Some(line) = line.cloned() { - table.get_config_mut().insert_horizontal_line(0, line); - if structure.with_footer { - let last_row = table.count_rows(); - table - .get_config_mut() - .insert_horizontal_line(last_row, line); - } - }; - } - - if structure.with_footer { - let last_row = table.count_rows(); - table.with(SetLineHeaders::new(last_row, head.clone())); - } - - table.with(SetLineHeaders::new(0, head)); -} - -fn truncate_table(table: &mut Table, cfg: TableConfig, widths: Vec, termwidth: usize) { - table.with(WidthCtrl::new(widths, cfg, termwidth)); -} - fn indent_sum(indent: TableIndent) -> usize { indent.left + indent.right } @@ -404,10 +303,75 @@ fn get_table_structure(data: &VecRecords>, cfg: &TableConfig) -> Ta TableStructure::new(with_index, with_header, with_footer) } +fn adjust_table(table: &mut Table, width_ctrl: WidthCtrl, border_header: bool, with_footer: bool) { + if border_header { + if with_footer { + set_border_head_with_footer(table, width_ctrl); + } else { + set_border_head(table, width_ctrl); + } + } else { + table.with(width_ctrl); + } +} + fn set_indent(table: &mut Table, indent: TableIndent) { table.with(Padding::new(indent.left, indent.right, 0, 0)); } +fn set_border_head(table: &mut Table, wctrl: WidthCtrl) { + let mut row = GetRow(0, Vec::new()); + let mut row_opts = GetRowSettings(0, AlignmentHorizontal::Left, None); + + table.with(&mut row); + table.with(&mut row_opts); + + table.with( + Settings::default() + .with(strip_color_from_row(0)) + .with(wctrl) + .with(MoveRowNext::new(0, 0)) + .with(SetLineHeaders::new(0, row.1, row_opts.1, row_opts.2)), + ); +} + +fn set_border_head_with_footer(table: &mut Table, wctrl: WidthCtrl) { + // note: funnily last and row must be equal at this point but we do not rely on it just in case. + + let count_rows = table.count_rows(); + let last_row_index = count_rows - 1; + + let mut first_row = GetRow(0, Vec::new()); + let mut head_settings = GetRowSettings(0, AlignmentHorizontal::Left, None); + let mut last_row = GetRow(last_row_index, Vec::new()); + + table.with(&mut first_row); + table.with(&mut head_settings); + table.with(&mut last_row); + + let head = first_row.1; + let footer = last_row.1; + let alignment = head_settings.1; + let head_color = head_settings.2.clone(); + let footer_color = head_settings.2; + + table.with( + Settings::default() + .with(strip_color_from_row(0)) + .with(strip_color_from_row(count_rows - 1)) + .with(wctrl) + .with(MoveRowNext::new(0, 0)) + .with(MoveRowPrev::new(last_row_index - 1, last_row_index)) + .with(SetLineHeaders::new(0, head, alignment, head_color)) + .with(SetLineHeaders::new( + last_row_index - 1, + footer, + alignment, + footer_color, + )), + ); +} + fn table_to_string(table: Table, termwidth: usize) -> Option { let total_width = table.total_width(); @@ -423,14 +387,16 @@ struct WidthCtrl { width: Vec, cfg: TableConfig, width_max: usize, + pad: usize, } impl WidthCtrl { - fn new(width: Vec, cfg: TableConfig, max: usize) -> Self { + fn new(width: Vec, cfg: TableConfig, max: usize, pad: usize) -> Self { Self { width, cfg, width_max: max, + pad, } } } @@ -448,9 +414,8 @@ impl TableOption> for if need_truncation { let has_header = self.cfg.structure.with_header && rec.count_rows() > 1; let as_head = has_header && self.cfg.header_on_border; - let pad = indent_sum(self.cfg.indent); - let trim = TableTrim::new(self.width, self.width_max, self.cfg.trim, as_head, pad); + let trim = TableTrim::new(self.width, self.width_max, self.cfg.trim, as_head, self.pad); trim.change(rec, cfg, dim); return; } @@ -698,22 +663,24 @@ fn load_theme( fn maybe_truncate_columns( data: &mut NuRecords, cfg: &TableConfig, - head: Option, termwidth: usize, + pad: usize, ) -> Vec { const TERMWIDTH_THRESHOLD: usize = 120; - let pad = cfg.indent.left + cfg.indent.right; - let preserve_content = termwidth > TERMWIDTH_THRESHOLD; + let has_header = cfg.structure.with_header && data.count_rows() > 1; + let is_header_on_border = has_header && cfg.header_on_border; - if let Some(head) = head { - truncate_columns_by_head(data, &cfg.theme, head, pad, termwidth) + let truncate = if is_header_on_border { + truncate_columns_by_head } else if preserve_content { - truncate_columns_by_columns(data, &cfg.theme, pad, termwidth) + truncate_columns_by_columns } else { - truncate_columns_by_content(data, &cfg.theme, pad, termwidth) - } + truncate_columns_by_content + }; + + truncate(data, &cfg.theme, pad, termwidth) } // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. @@ -868,76 +835,51 @@ fn truncate_columns_by_columns( widths } -// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but -// WITH AS MUCH CONTENT AS POSSIBLE BY ACCOUNTED BY HEADERS. +// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. fn truncate_columns_by_head( data: &mut NuRecords, theme: &TableTheme, - head: HeadInfo, pad: usize, termwidth: usize, ) -> Vec { - const MIN_ACCEPTABLE_WIDTH: usize = 3; const TRAILING_COLUMN_WIDTH: usize = 5; - if data.is_empty() { - return vec![0; data.count_columns()]; + let config = create_config(theme, false, None); + let mut widths = build_width(&*data, pad); + let total_width = get_total_width2(&widths, &config); + if total_width <= termwidth { + return widths; } - let mut widths = build_width(data, pad); + if data.is_empty() { + return widths; + } + + let head = &data[0]; - let config = create_config(theme, false, None); let borders = config.get_borders(); let has_vertical = borders.has_vertical(); let mut width = borders.has_left() as usize + borders.has_right() as usize; let mut truncate_pos = 0; - for (i, head) in head.values.iter().enumerate() { - let head_width = string_width(head); - let col_width = widths[i]; - if head_width + pad <= col_width { - let move_width = head_width + pad + (i > 0 && has_vertical) as usize; - if width + move_width >= termwidth { - break; - } + for (i, column_header) in head.iter().enumerate() { + let column_header_width = Cell::width(column_header); + width += column_header_width + pad; - width += move_width; - truncate_pos += 1; - continue; + if i > 0 { + width += has_vertical as usize; } - // NOTE: So header is bigger then a column - // Therefore we must try to expand the column to head text width as much as possible. - // - // The kicker is that we will truncate the header if we can't fit it totally. - // Therefore it's not guaranteed that the column will be expanded to exactly head width. - widths[i] = head_width + pad; - let col_width = widths[i]; - - let move_width = col_width + (i > 0 && has_vertical) as usize; - if width + move_width >= termwidth { - let mut used_width = width + pad + (i > 0 && has_vertical) as usize; - if i + 1 != widths.len() { - used_width += TRAILING_COLUMN_WIDTH; - } - - let available = termwidth.saturating_sub(used_width); - - if available > MIN_ACCEPTABLE_WIDTH { - width += available; - widths[i] = available; - truncate_pos += 1; - } - + if width >= termwidth { + width -= column_header_width + (i > 0 && has_vertical) as usize + pad; break; } - width += move_width; truncate_pos += 1; } // we don't need any truncation then (is it possible?) - if truncate_pos == head.values.len() { + if truncate_pos == head.len() { return widths; } @@ -1049,16 +991,63 @@ fn build_width(records: &NuRecords, pad: usize) -> Vec { widths } +struct GetRow(usize, Vec); + +impl TableOption> for &mut GetRow { + fn change( + self, + recs: &mut NuRecords, + _: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = recs[row].iter().map(|c| c.as_ref().to_owned()).collect(); + } +} + +struct GetRowSettings(usize, AlignmentHorizontal, Option); + +impl TableOption> + for &mut GetRowSettings +{ + fn change( + self, + _: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = *cfg.get_alignment_horizontal(Entity::Row(row)); + self.2 = cfg + .get_colors() + .get_color((row, 0)) + .cloned() + .map(Color::from); + } +} + // It's laverages a use of guuaranted cached widths before hand // to speed up things a bit. struct SetLineHeaders { line: usize, - head: HeadInfo, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, } impl SetLineHeaders { - fn new(line: usize, head: HeadInfo) -> Self { - Self { line, head } + fn new( + line: usize, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, + ) -> Self { + Self { + line, + columns, + alignment, + color, + } } } @@ -1082,8 +1071,7 @@ impl TableOption> for }; let columns: Vec<_> = self - .head - .values + .columns .into_iter() .zip(widths.iter().cloned()) // it must be always safe to do .map(|(s, width)| Truncate::truncate(&s, width).into_owned()) @@ -1091,8 +1079,8 @@ impl TableOption> for let mut names = ColumnNames::new(columns) .line(self.line) - .alignment(Alignment::from(self.head.align)); - if let Some(color) = self.head.color { + .alignment(Alignment::from(self.alignment)); + if let Some(color) = self.color { names = names.color(color); } @@ -1104,25 +1092,163 @@ impl TableOption> for } } -fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) { - if let Some(line) = theme.get_horizontal_line(from) { - theme.insert_horizontal_line(to, *line); +struct MoveRowNext { + row: usize, + line: usize, +} + +impl MoveRowNext { + fn new(row: usize, line: usize) -> Self { + Self { row, line } } } -struct GetDims(Vec); +struct MoveRowPrev { + row: usize, + line: usize, +} -impl TableOption> for &mut GetDims { +impl MoveRowPrev { + fn new(row: usize, line: usize) -> Self { + Self { row, line } + } +} + +impl TableOption> for MoveRowNext { fn change( self, - _: &mut NuRecords, - _: &mut ColoredConfig, - dims: &mut CompleteDimensionVecRecords<'_>, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, ) { - self.0 = dims.get_widths().expect("expected to get it").to_vec(); + row_shift_next(recs, cfg, self.row, self.line); } fn hint_change(&self) -> Option { None } } + +impl TableOption> for MoveRowPrev { + fn change( + self, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + row_shift_prev(recs, cfg, self.row, self.line); + } + + fn hint_change(&self) -> Option { + None + } +} + +fn row_shift_next(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { + let count_rows = recs.count_rows(); + let count_columns = recs.count_columns(); + let has_line = cfg.has_horizontal(line, count_rows); + let has_next_line = cfg.has_horizontal(line + 1, count_rows); + if !has_line && !has_next_line { + return; + } + + recs.remove_row(row); + let count_rows = recs.count_rows(); + + shift_alignments_down(cfg, row, count_rows, count_columns); + shift_colors_down(cfg, row, count_rows, count_columns); + + if !has_line { + shift_lines_up(cfg, count_rows, &[line + 1]); + } else { + remove_lines(cfg, count_rows, &[line + 1]); + } + + shift_lines_up(cfg, count_rows, &[count_rows]); +} + +fn row_shift_prev(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { + let mut count_rows = recs.count_rows(); + let count_columns = recs.count_columns(); + let has_line = cfg.has_horizontal(line, count_rows); + let has_prev_line = cfg.has_horizontal(line - 1, count_rows); + if !has_line && !has_prev_line { + return; + } + + recs.remove_row(row); + + if !has_line { + return; + } + + count_rows -= 1; + + shift_alignments_down(cfg, row, count_rows, count_columns); + shift_colors_down(cfg, row, count_rows, count_columns); + remove_lines(cfg, count_rows, &[line - 1]); +} + +fn remove_lines(cfg: &mut ColoredConfig, count_rows: usize, line: &[usize]) { + for &line in line { + cfg.remove_horizontal_line(line, count_rows) + } +} + +fn shift_alignments_down( + cfg: &mut ColoredConfig, + row: usize, + count_rows: usize, + count_columns: usize, +) { + for row in row..count_rows { + for col in 0..count_columns { + let pos = (row + 1, col).into(); + let posn = (row, col).into(); + let align = *cfg.get_alignment_horizontal(pos); + cfg.set_alignment_horizontal(posn, align); + } + + let align = *cfg.get_alignment_horizontal(Entity::Row(row + 1)); + cfg.set_alignment_horizontal(Entity::Row(row), align); + } +} + +fn shift_colors_down(cfg: &mut ColoredConfig, row: usize, count_rows: usize, count_columns: usize) { + for row in row..count_rows { + for col in 0..count_columns { + let pos = (row + 1, col); + let posn = (row, col).into(); + let color = cfg.get_colors().get_color(pos).cloned(); + if let Some(color) = color { + cfg.set_color(posn, color); + } + } + } +} + +fn shift_lines_up(cfg: &mut ColoredConfig, count_rows: usize, lines: &[usize]) { + for &i in lines { + let line = cfg.get_horizontal_line(i).cloned(); + if let Some(line) = line { + cfg.insert_horizontal_line(i - 1, line); + cfg.remove_horizontal_line(i, count_rows); + } + } +} + +fn theme_copy_horizontal_line(theme: &mut tabled::settings::Theme, from: usize, to: usize) { + if let Some(line) = theme.get_horizontal_line(from) { + theme.insert_horizontal_line(to, *line); + } +} + +#[allow(clippy::type_complexity)] +fn strip_color_from_row(row: usize) -> ModifyList String>> { + fn foo(s: &str) -> String { + strip_ansi_unlikely(s).into_owned() + } + + Modify::new(Rows::single(row)).with(Format::content(foo)) +} diff --git a/crates/nu-table/src/types/expanded.rs b/crates/nu-table/src/types/expanded.rs index c93832fc19..393a381646 100644 --- a/crates/nu-table/src/types/expanded.rs +++ b/crates/nu-table/src/types/expanded.rs @@ -240,7 +240,7 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } let mut available = available_width - pad_space; - let mut column_width = 0; + let mut column_width = string_width(&header); if !is_last_column { // we need to make sure that we have a space for a next column if we use available width @@ -293,18 +293,9 @@ fn expand_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { column_rows = column_rows.saturating_add(cell.size); } - let mut head_width = string_width(&header); - let mut header = header; - if head_width > available { - header = wrap_text(&header, available, cfg.opts.config); - head_width = available; - } - let head_cell = NuRecordsValue::new(header); data[0].push(head_cell); - column_width = max(column_width, head_width); - if column_width > available { // remove the column we just inserted for row in &mut data { diff --git a/crates/nu-table/src/util.rs b/crates/nu-table/src/util.rs index 8784454cfb..8384ef144f 100644 --- a/crates/nu-table/src/util.rs +++ b/crates/nu-table/src/util.rs @@ -31,27 +31,6 @@ pub fn string_wrap(text: &str, width: usize, keep_words: bool) -> String { Wrap::wrap(text, width, keep_words) } -pub fn string_expand(text: &str, width: usize) -> String { - use std::{borrow::Cow, iter::repeat}; - use tabled::grid::util::string::{get_line_width, get_lines}; - - get_lines(text) - .map(|line| { - let length = get_line_width(&line); - - if length < width { - let mut line = line.into_owned(); - let remain = width - length; - line.extend(repeat(' ').take(remain)); - Cow::Owned(line) - } else { - line - } - }) - .collect::>() - .join("\n") -} - pub fn string_truncate(text: &str, width: usize) -> String { let line = match text.lines().next() { Some(line) => line, diff --git a/typos.toml b/typos.toml index 76d8b8959a..14baa39811 100644 --- a/typos.toml +++ b/typos.toml @@ -1,7 +1,6 @@ [files] extend-exclude = [ ".git/", - "crates/nu-command/tests/commands/table.rs", "crates/nu-cmd-extra/assets/228_themes.json", "tests/fixtures/formats/", ] From 67ea25afcac340ae499a9f69ce542b7b6772185a Mon Sep 17 00:00:00 2001 From: Carson Riker Date: Sat, 5 Apr 2025 17:31:05 -0400 Subject: [PATCH 05/13] Limit Allowed `serde_json` Versions to Match Usage (#15504) Fixes #15503 # Description Our usage of `serde_json::Error::io_error_kind` is improperly handled in the workspace version specifier. We use this method in `nu-plugin-core` https://github.com/nushell/nushell/blob/f25525be6cc0343d77f232f8ccecde25798d56aa/crates/nu-plugin-core/src/serializers/json.rs#L77-L106 It was added in [`serde_json` v1.0.97](https://github.com/serde-rs/json/releases/tag/v1.0.97). Previously, we specified our version requirement only as `1.0`. Now, it is `>=1.0.97,<1.1`, which correctly describes our maximum range of compatibility. # User-Facing Changes None # Tests + Formatting No code has changed. Recent releases are identical. This only effect usage of nushell as a library # After Submitting No doc changes should be needed. This prevents certain compiler errors, but will not change the behavior of any compiled project. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9482c837cf..a5824b7c93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ rusqlite = "0.31" rust-embed = "8.6.0" scopeguard = { version = "1.2.0" } serde = { version = "1.0" } -serde_json = "1.0" +serde_json = "1.0.97" serde_urlencoded = "0.7.1" serde_yaml = "0.9.33" sha2 = "0.10" From 1c6c85d35dc3eaff682c1b6fe727501e2050b417 Mon Sep 17 00:00:00 2001 From: Wind Date: Sun, 6 Apr 2025 09:49:28 +0800 Subject: [PATCH 06/13] Fix clippy (#15489) # Description There are some clippy(version 0.1.86) errors on nushell repo. This pr is trying to fix it. # User-Facing Changes Hopefully none. # Tests + Formatting NaN # After Submitting NaN --- crates/nu-cmd-extra/src/extra/bits/mod.rs | 2 +- crates/nu-cmd-extra/src/extra/bits/shift_left.rs | 2 +- crates/nu-command/src/debug/inspect_table.rs | 2 +- crates/nu-command/src/filesystem/ls.rs | 5 +---- crates/nu-command/src/filters/chunks.rs | 4 ++-- crates/nu-command/src/math/utils.rs | 2 +- crates/nu-command/src/network/http/client.rs | 2 +- crates/nu-command/src/path/join.rs | 2 +- crates/nu-engine/src/compile/redirect.rs | 2 +- .../nu-explore/src/views/binary/binary_widget.rs | 4 +--- crates/nu-parser/src/parser.rs | 2 +- crates/nu-path/src/dots.rs | 4 ++-- crates/nu-path/src/tilde.rs | 4 ++-- .../nu-protocol/src/engine/state_working_set.rs | 4 ++-- crates/nu-protocol/src/lev_distance.rs | 2 +- crates/nu-protocol/src/value/from_value.rs | 2 +- crates/nu-table/src/unstructured_table.rs | 2 +- crates/nu-term-grid/src/grid.rs | 16 ++++++++-------- .../src/dataframe/values/nu_dataframe/mod.rs | 2 +- 19 files changed, 30 insertions(+), 35 deletions(-) diff --git a/crates/nu-cmd-extra/src/extra/bits/mod.rs b/crates/nu-cmd-extra/src/extra/bits/mod.rs index 437313960e..cbac833896 100644 --- a/crates/nu-cmd-extra/src/extra/bits/mod.rs +++ b/crates/nu-cmd-extra/src/extra/bits/mod.rs @@ -135,7 +135,7 @@ where (min, max) => (rhs, lhs, max, min), }; - let pad = iter::repeat(0).take(max_len - min_len); + let pad = iter::repeat_n(0, max_len - min_len); let mut a; let mut b; diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs index 0f1f0114e1..5b8e6a5ae6 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs @@ -249,7 +249,7 @@ fn shift_bytes_and_bits_left(data: &[u8], byte_shift: usize, bit_shift: usize) - Last | Only => lhs << bit_shift, _ => (lhs << bit_shift) | (rhs >> (8 - bit_shift)), }) - .chain(iter::repeat(0).take(byte_shift)) + .chain(iter::repeat_n(0, byte_shift)) .collect::>() } diff --git a/crates/nu-command/src/debug/inspect_table.rs b/crates/nu-command/src/debug/inspect_table.rs index 5f18eb5be3..6bd5a3cce8 100644 --- a/crates/nu-command/src/debug/inspect_table.rs +++ b/crates/nu-command/src/debug/inspect_table.rs @@ -118,7 +118,7 @@ fn increase_string_width(text: &mut String, total: usize) { let rest = total - width; if rest > 0 { - text.extend(std::iter::repeat(' ').take(rest)); + text.extend(std::iter::repeat_n(' ', rest)); } } diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index cda0fc0826..4a188429f3 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -378,10 +378,7 @@ fn ls_for_one_pattern( .par_bridge() .filter_map(move |x| match x { Ok(path) => { - let metadata = match std::fs::symlink_metadata(&path) { - Ok(metadata) => Some(metadata), - Err(_) => None, - }; + let metadata = std::fs::symlink_metadata(&path).ok(); let hidden_dir_clone = Arc::clone(&hidden_dirs); let mut hidden_dir_mutex = hidden_dir_clone .lock() diff --git a/crates/nu-command/src/filters/chunks.rs b/crates/nu-command/src/filters/chunks.rs index bfac65e358..2ff9b1b0a7 100644 --- a/crates/nu-command/src/filters/chunks.rs +++ b/crates/nu-command/src/filters/chunks.rs @@ -243,7 +243,7 @@ mod test { let chunks = chunk_read.map(|e| e.unwrap()).collect::>(); assert_eq!( chunks, - [s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()] + [&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]] ); } @@ -260,7 +260,7 @@ mod test { let chunks = chunk_read.map(|e| e.unwrap()).collect::>(); assert_eq!( chunks, - [s[..4].as_bytes(), s[4..8].as_bytes(), s[8..].as_bytes()] + [&s.as_bytes()[..4], &s.as_bytes()[4..8], &s.as_bytes()[8..]] ); } diff --git a/crates/nu-command/src/math/utils.rs b/crates/nu-command/src/math/utils.rs index 3035e78e02..9041f23ffa 100644 --- a/crates/nu-command/src/math/utils.rs +++ b/crates/nu-command/src/math/utils.rs @@ -102,7 +102,7 @@ pub fn calculate( mf(&new_vals?, span, name) } PipelineData::Value(val, ..) => mf(&[val], span, name), - PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: name }), + PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: name }), val => Err(ShellError::UnsupportedInput { msg: "Only ints, floats, lists, records, or ranges are supported".into(), input: "value originates from here".into(), diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index 271c521715..c4fe4f9486 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -723,7 +723,7 @@ fn transform_response_using_content_type( ) })? .path_segments() - .and_then(|segments| segments.last()) + .and_then(|mut segments| segments.next_back()) .and_then(|name| if name.is_empty() { None } else { Some(name) }) .and_then(|name| { PathBuf::from(name) diff --git a/crates/nu-command/src/path/join.rs b/crates/nu-command/src/path/join.rs index 9f055589d3..c36b51fb99 100644 --- a/crates/nu-command/src/path/join.rs +++ b/crates/nu-command/src/path/join.rs @@ -175,7 +175,7 @@ fn run(call: &Call, args: &Arguments, input: PipelineData) -> Result Err(ShellError::PipelineEmpty { dst_span: head }), + PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: head }), _ => Err(ShellError::UnsupportedInput { msg: "Input value cannot be joined".to_string(), input: "value originates from here".into(), diff --git a/crates/nu-engine/src/compile/redirect.rs b/crates/nu-engine/src/compile/redirect.rs index 101cc9077c..8d780177d9 100644 --- a/crates/nu-engine/src/compile/redirect.rs +++ b/crates/nu-engine/src/compile/redirect.rs @@ -98,7 +98,7 @@ pub(crate) fn finish_redirection( if !matches!( modes.err, Some(Spanned { - item: RedirectMode::Pipe { .. }, + item: RedirectMode::Pipe, .. }) ) { diff --git a/crates/nu-explore/src/views/binary/binary_widget.rs b/crates/nu-explore/src/views/binary/binary_widget.rs index d100774be5..859661ca02 100644 --- a/crates/nu-explore/src/views/binary/binary_widget.rs +++ b/crates/nu-explore/src/views/binary/binary_widget.rs @@ -323,9 +323,7 @@ fn repeat_vertical( c: char, style: TextStyle, ) { - let text = std::iter::repeat(c) - .take(width as usize) - .collect::(); + let text = std::iter::repeat_n(c, width as usize).collect::(); let style = text_style_to_tui_style(style); let span = Span::styled(text, style); diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index ec971639c5..9c4d2b30c4 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2678,7 +2678,7 @@ pub fn parse_unit_value<'res>( if let Some((unit, name, convert)) = unit_groups.iter().find(|x| value.ends_with(x.1)) { let lhs_len = value.len() - name.len(); - let lhs = strip_underscores(value[..lhs_len].as_bytes()); + let lhs = strip_underscores(&value.as_bytes()[..lhs_len]); let lhs_span = Span::new(span.start, span.start + lhs_len); let unit_span = Span::new(span.start + lhs_len, span.end); if lhs.ends_with('$') { diff --git a/crates/nu-path/src/dots.rs b/crates/nu-path/src/dots.rs index 97a7deac02..bfbb027d9a 100644 --- a/crates/nu-path/src/dots.rs +++ b/crates/nu-path/src/dots.rs @@ -46,7 +46,7 @@ pub fn expand_ndots(path: impl AsRef) -> PathBuf { pub fn expand_dots(path: impl AsRef) -> PathBuf { // Check if the last component of the path is a normal component. fn last_component_is_normal(path: &Path) -> bool { - matches!(path.components().last(), Some(Component::Normal(_))) + matches!(path.components().next_back(), Some(Component::Normal(_))) } let path = path.as_ref(); @@ -61,7 +61,7 @@ pub fn expand_dots(path: impl AsRef) -> PathBuf { // no-op } _ => { - let prev_component = result.components().last(); + let prev_component = result.components().next_back(); if prev_component == Some(Component::RootDir) && component == Component::ParentDir { continue; } diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs index 5bae92424a..59c14e5986 100644 --- a/crates/nu-path/src/tilde.rs +++ b/crates/nu-path/src/tilde.rs @@ -29,7 +29,7 @@ fn expand_tilde_with_home(path: impl AsRef, home: Option) -> Path }; } - let path_last_char = path.as_os_str().to_string_lossy().chars().last(); + let path_last_char = path.as_os_str().to_string_lossy().chars().next_back(); let need_trailing_slash = path_last_char == Some('/') || path_last_char == Some('\\'); match home { @@ -94,7 +94,7 @@ fn user_home_dir(username: &str) -> PathBuf { if !cfg!(target_os = "android") && expected_path .components() - .last() + .next_back() .map(|last| last != Component::Normal(username.as_ref())) .unwrap_or(false) { diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index 11be80efb9..fb32d54231 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -884,7 +884,7 @@ impl<'a> StateWorkingSet<'a> { .active_overlay_names(&mut removed_overlays) .iter() .rev() - .last() + .next_back() { return last_name; } @@ -900,7 +900,7 @@ impl<'a> StateWorkingSet<'a> { if let Some(last_overlay) = scope_frame .active_overlays(&mut removed_overlays) .rev() - .last() + .next_back() { return last_overlay; } diff --git a/crates/nu-protocol/src/lev_distance.rs b/crates/nu-protocol/src/lev_distance.rs index 51dc856d86..77bc20fd65 100644 --- a/crates/nu-protocol/src/lev_distance.rs +++ b/crates/nu-protocol/src/lev_distance.rs @@ -88,7 +88,7 @@ pub fn lev_distance_with_substrings(a: &str, b: &str, limit: usize) -> Option() .split(':') - .last() + .next_back() .expect("str::split returns an iterator with at least one element") .to_string() .into_boxed_str(), diff --git a/crates/nu-table/src/unstructured_table.rs b/crates/nu-table/src/unstructured_table.rs index be722c5e51..1ce5f164ee 100644 --- a/crates/nu-table/src/unstructured_table.rs +++ b/crates/nu-table/src/unstructured_table.rs @@ -119,7 +119,7 @@ fn build_vertical_map(record: Record, config: &Config) -> TableValue { fn string_append_to_width(key: &mut String, max: usize) { let width = string_width(key); let rest = max - width; - key.extend(std::iter::repeat(' ').take(rest)); + key.extend(std::iter::repeat_n(' ', rest)); } fn build_vertical_array(vals: Vec, config: &Config) -> TableValue { diff --git a/crates/nu-term-grid/src/grid.rs b/crates/nu-term-grid/src/grid.rs index 4c92d6003d..f7881a3383 100644 --- a/crates/nu-term-grid/src/grid.rs +++ b/crates/nu-term-grid/src/grid.rs @@ -39,15 +39,15 @@ //! that dictate how the grid is formatted: //! //! - `filling`: what to put in between two columns — either a number of -//! spaces, or a text string; +//! spaces, or a text string; //! - `direction`, which specifies whether the cells should go along -//! rows, or columns: +//! rows, or columns: //! - `Direction::LeftToRight` starts them in the top left and -//! moves *rightwards*, going to the start of a new row after reaching the -//! final column; +//! moves *rightwards*, going to the start of a new row after reaching the +//! final column; //! - `Direction::TopToBottom` starts them in the top left and moves -//! *downwards*, going to the top of a new column after reaching the final -//! row. +//! *downwards*, going to the top of a new column after reaching the final +//! row. //! //! //! ## Displaying a grid @@ -93,7 +93,7 @@ use std::cmp::max; use std::fmt; -use std::iter::repeat; +use std::iter::repeat_n; use unicode_width::UnicodeWidthStr; fn unicode_width_strip_ansi(astring: &str) -> usize { @@ -290,7 +290,7 @@ impl Grid { } fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions { - let mut widths: Vec = repeat(0).take(num_columns).collect(); + let mut widths: Vec = repeat_n(0, num_columns).collect(); for (index, cell) in self.cells.iter().enumerate() { let index = match self.options.direction { Direction::LeftToRight => index % num_columns, 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 55c1f620c9..e4191012c7 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 @@ -317,7 +317,7 @@ impl NuDataFrame { let series = self.as_series(span)?; let column = conversion::create_column_from_series(&series, row, row + 1, span)?; - if column.len() == 0 { + if column.is_empty() { Err(ShellError::AccessEmptyContent { span }) } else { let value = column From b81d46574ca487f2f1dda8fb5a02fc6ecc529289 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:24:55 +0200 Subject: [PATCH 07/13] build(deps): bump openssl from 0.10.70 to 0.10.72 (#15493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.70 to 0.10.72.
Release notes

Sourced from openssl's releases.

openssl-v0.10.72

What's Changed

New Contributors

Full Changelog: https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72

openssl-v0.10.71

What's Changed

New Contributors

Full Changelog: https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.70...openssl-v0.10.71

Commits
  • 87085bd Merge pull request #2390 from alex/uaf-fix
  • d1a12e2 Fixed two UAFs and bumped versions for release
  • 7c7b2e6 Merge pull request #2389 from skmcgrail/aws-lc-follow-up
  • 34a477b Use --experimental with bindgen-cli with aws-lc build
  • d4bf071 Merge pull request #2386 from skmcgrail/aws-lc-follow-up
  • a86bf67 Remove comment
  • 705dbfb Fix test
  • e0df413 Skip final call for LibreSSL 4.1.0 for CCM mode
  • 2f1164b Enable additional capabilities for AWS-LC
  • dde9ffb Merge pull request #1805 from skmcgrail/aws-lc-support-final
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=openssl&package-manager=cargo&previous-version=0.10.70&new-version=0.10.72)](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 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2995f0e0ed..a78d62f77c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4488,9 +4488,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.70" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -4529,9 +4529,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.105" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", From eb2a91ea7c15eb413634c63d5d58eb7a6845b474 Mon Sep 17 00:00:00 2001 From: zc he Date: Sun, 6 Apr 2025 21:36:59 +0800 Subject: [PATCH 08/13] fix(lsp): keywords in completion snippets (#15499) # Description Fixes some leftover issues for keyword snippets of #15494 # Tests + Formatting Adjusted --- crates/nu-lsp/src/completion.rs | 214 ++++++++++++++++++-------------- 1 file changed, 118 insertions(+), 96 deletions(-) diff --git a/crates/nu-lsp/src/completion.rs b/crates/nu-lsp/src/completion.rs index a3650c5f32..cd354dd927 100644 --- a/crates/nu-lsp/src/completion.rs +++ b/crates/nu-lsp/src/completion.rs @@ -4,11 +4,11 @@ use crate::{span_to_range, uri_to_path, LanguageServer}; use lsp_types::{ CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams, CompletionResponse, CompletionTextEdit, Documentation, InsertTextFormat, MarkupContent, - MarkupKind, TextEdit, + MarkupKind, Range, TextEdit, }; -use nu_cli::{NuCompleter, SuggestionKind}; +use nu_cli::{NuCompleter, SemanticSuggestion, SuggestionKind}; use nu_protocol::{ - engine::{CommandType, Stack}, + engine::{CommandType, EngineState, Stack}, PositionalArg, Span, SyntaxShape, }; @@ -46,101 +46,123 @@ impl LanguageServer { results .into_iter() .map(|r| { - let decl_id = r.kind.as_ref().and_then(|kind| { - matches!(kind, SuggestionKind::Command(_)) - .then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?) - }); - - let mut snippet_text = r.suggestion.value.clone(); - let mut doc_string = r.suggestion.extra.map(|ex| ex.join("\n")); - let mut insert_text_format = None; - let mut idx = 1; - // use snippet as `insert_text_format` for command argument completion - if let Some(decl_id) = decl_id { - let cmd = engine_state.get_decl(decl_id); - doc_string = Some(Self::get_decl_description(cmd, true)); - insert_text_format = Some(InsertTextFormat::SNIPPET); - let signature = cmd.signature(); - // add curly brackets around block arguments - let block_wrapper = |arg: &PositionalArg, text: String| -> String { - if matches!(arg.shape, SyntaxShape::Block | SyntaxShape::MatchBlock) { - format!("{{ {text} }}") - } else { - text - } - }; - - for required in signature.required_positional { - snippet_text.push(' '); - snippet_text.push_str( - block_wrapper(&required, format!("${{{}:{}}}", idx, required.name)) - .as_str(), - ); - idx += 1; - } - for optional in signature.optional_positional { - snippet_text.push(' '); - snippet_text.push_str( - block_wrapper( - &optional, - format!("${{{}:{}?}}", idx, optional.name), - ) - .as_str(), - ); - idx += 1; - } - if let Some(rest) = signature.rest_positional { - snippet_text - .push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str()); - idx += 1; - } - } - // no extra space for a command with args expanded in the snippet - if idx == 1 && r.suggestion.append_whitespace { - snippet_text.push(' '); - } - - let span = r.suggestion.span; - let text_edit = Some(CompletionTextEdit::Edit(TextEdit { - range: span_to_range(&Span::new(span.start, span.end), file, 0), - new_text: snippet_text, - })); - - CompletionItem { - label: r.suggestion.value, - label_details: r - .kind - .as_ref() - .map(|kind| match kind { - SuggestionKind::Value(t) => t.to_string(), - SuggestionKind::Command(cmd) => cmd.to_string(), - SuggestionKind::Module => "module".to_string(), - SuggestionKind::Operator => "operator".to_string(), - SuggestionKind::Variable => "variable".to_string(), - SuggestionKind::Flag => "flag".to_string(), - _ => String::new(), - }) - .map(|s| CompletionItemLabelDetails { - detail: None, - description: Some(s), - }), - detail: r.suggestion.description, - documentation: doc_string.map(|value| { - Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value, - }) - }), - kind: Self::lsp_completion_item_kind(r.kind), - text_edit, - insert_text_format, - ..Default::default() - } + let reedline_span = r.suggestion.span; + Self::completion_item_from_suggestion( + &engine_state, + r, + span_to_range(&Span::new(reedline_span.start, reedline_span.end), file, 0), + ) }) .collect(), )) } + fn completion_item_from_suggestion( + engine_state: &EngineState, + suggestion: SemanticSuggestion, + range: Range, + ) -> CompletionItem { + let decl_id = suggestion.kind.as_ref().and_then(|kind| { + matches!(kind, SuggestionKind::Command(_)) + .then_some(engine_state.find_decl(suggestion.suggestion.value.as_bytes(), &[])?) + }); + + let mut snippet_text = suggestion.suggestion.value.clone(); + let mut doc_string = suggestion.suggestion.extra.map(|ex| ex.join("\n")); + let mut insert_text_format = None; + let mut idx = 0; + // use snippet as `insert_text_format` for command argument completion + if let Some(decl_id) = decl_id { + let cmd = engine_state.get_decl(decl_id); + doc_string = Some(Self::get_decl_description(cmd, true)); + insert_text_format = Some(InsertTextFormat::SNIPPET); + let signature = cmd.signature(); + // add curly brackets around block arguments + // and keywords, e.g. `=` in `alias foo = bar` + let mut arg_wrapper = |arg: &PositionalArg, text: String, optional: bool| -> String { + idx += 1; + match &arg.shape { + SyntaxShape::Block | SyntaxShape::MatchBlock => { + format!("{{ ${{{}:{}}} }}", idx, text) + } + SyntaxShape::Keyword(kwd, _) => { + // NOTE: If optional, the keyword should also be in a placeholder so that it can be removed easily. + // Here we choose to use nested placeholders. Note that some editors don't fully support this format, + // but usually they will simply ignore the inner ones, so it should be fine. + if optional { + idx += 1; + format!( + "${{{}:{} ${{{}:{}}}}}", + idx - 1, + String::from_utf8_lossy(kwd), + idx, + text + ) + } else { + format!("{} ${{{}:{}}}", String::from_utf8_lossy(kwd), idx, text) + } + } + _ => format!("${{{}:{}}}", idx, text), + } + }; + + for required in signature.required_positional { + snippet_text.push(' '); + snippet_text + .push_str(arg_wrapper(&required, required.name.clone(), false).as_str()); + } + for optional in signature.optional_positional { + snippet_text.push(' '); + snippet_text + .push_str(arg_wrapper(&optional, format!("{}?", optional.name), true).as_str()); + } + if let Some(rest) = signature.rest_positional { + idx += 1; + snippet_text.push_str(format!(" ${{{}:...{}}}", idx, rest.name).as_str()); + } + } + // no extra space for a command with args expanded in the snippet + if idx == 0 && suggestion.suggestion.append_whitespace { + snippet_text.push(' '); + } + + let text_edit = Some(CompletionTextEdit::Edit(TextEdit { + range, + new_text: snippet_text, + })); + + CompletionItem { + label: suggestion.suggestion.value, + label_details: suggestion + .kind + .as_ref() + .map(|kind| match kind { + SuggestionKind::Value(t) => t.to_string(), + SuggestionKind::Command(cmd) => cmd.to_string(), + SuggestionKind::Module => "module".to_string(), + SuggestionKind::Operator => "operator".to_string(), + SuggestionKind::Variable => "variable".to_string(), + SuggestionKind::Flag => "flag".to_string(), + _ => String::new(), + }) + .map(|s| CompletionItemLabelDetails { + detail: None, + description: Some(s), + }), + detail: suggestion.suggestion.description, + documentation: doc_string.map(|value| { + Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value, + }) + }), + kind: Self::lsp_completion_item_kind(suggestion.kind), + text_edit, + insert_text_format, + ..Default::default() + } + } + fn lsp_completion_item_kind( suggestion_kind: Option, ) -> Option { @@ -349,7 +371,7 @@ mod tests { "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, - "newText": "alias ${1:name} ${2:initial_value}" + "newText": "alias ${1:name} = ${2:initial_value}" }, "insertTextFormat": 2, "kind": 14 @@ -367,7 +389,7 @@ mod tests { "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 2 }, }, - "newText": "alias ${1:name} ${2:initial_value}" + "newText": "alias ${1:name} = ${2:initial_value}" }, "insertTextFormat": 2, "kind": 14 @@ -530,7 +552,7 @@ mod tests { "detail": "Alias a command (with optional flags) to a new name.", "textEdit": { "range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 5 }, }, - "newText": "alias ${1:name} ${2:initial_value}" + "newText": "alias ${1:name} = ${2:initial_value}" }, "kind": 14 }, From 41f4d0dcbcd07401a303dfd939046bdd00c2b3ab Mon Sep 17 00:00:00 2001 From: zc he Date: Sun, 6 Apr 2025 21:37:59 +0800 Subject: [PATCH 09/13] refactor(lsp): align markdown doc string with output of --help (#15508) #15499 reminds me of the discrepancies between lsp hover docs and `--help` outputs. # Description # User-Facing Changes Before: image After: image Output of `if -h` as a reference: ``` Usage: > if (else ) Flags: -h, --help: Display the help message for this command Parameters: cond : Condition to check. then_block : Block to run if check succeeds. "else" + : Expression or block to run when the condition is false. (optional) ``` # Tests + Formatting Refined # After Submitting --- crates/nu-lsp/src/hover.rs | 83 ++++++------ crates/nu-lsp/src/notification.rs | 2 +- crates/nu-lsp/src/signature.rs | 173 ++++++++++++++++++-------- tests/fixtures/lsp/hints/signature.nu | 4 +- 4 files changed, 157 insertions(+), 105 deletions(-) diff --git a/crates/nu-lsp/src/hover.rs b/crates/nu-lsp/src/hover.rs index 4971ada75a..b2eee0f60d 100644 --- a/crates/nu-lsp/src/hover.rs +++ b/crates/nu-lsp/src/hover.rs @@ -1,7 +1,10 @@ use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; -use nu_protocol::engine::Command; +use nu_protocol::{engine::Command, PositionalArg}; -use crate::{Id, LanguageServer}; +use crate::{ + signature::{display_flag, doc_for_arg, get_signature_label}, + Id, LanguageServer, +}; impl LanguageServer { pub(crate) fn get_decl_description(decl: &dyn Command, skip_description: bool) -> String { @@ -19,35 +22,27 @@ impl LanguageServer { // Usage description.push_str("---\n### Usage \n```nu\n"); let signature = decl.signature(); - description.push_str(&Self::get_signature_label(&signature)); + description.push_str(&get_signature_label(&signature, true)); description.push_str("\n```\n"); // Flags if !signature.named.is_empty() { description.push_str("\n### Flags\n\n"); let mut first = true; - for named in &signature.named { + for named in signature.named { if first { first = false; } else { description.push('\n'); } description.push_str(" "); - if let Some(short_flag) = &named.short { - description.push_str(&format!("`-{short_flag}`")); - } - if !named.long.is_empty() { - if named.short.is_some() { - description.push_str(", "); - } - description.push_str(&format!("`--{}`", named.long)); - } - if let Some(arg) = &named.arg { - description.push_str(&format!(" `<{}>`", arg.to_type())); - } - if !named.desc.is_empty() { - description.push_str(&format!(" - {}", named.desc)); - } + description.push_str(&display_flag(&named, true)); + description.push_str(&doc_for_arg( + named.arg, + named.desc, + named.default_value, + false, + )); description.push('\n'); } description.push('\n'); @@ -60,46 +55,38 @@ impl LanguageServer { { description.push_str("\n### Parameters\n\n"); let mut first = true; - for required_arg in &signature.required_positional { + let mut write_arg = |arg: PositionalArg, optional: bool| { if first { first = false; } else { description.push('\n'); } - description.push_str(&format!( - " `{}: {}`", - required_arg.name, - required_arg.shape.to_type() + description.push_str(&format!(" `{}`", arg.name)); + description.push_str(&doc_for_arg( + Some(arg.shape), + arg.desc, + arg.default_value, + optional, )); - if !required_arg.desc.is_empty() { - description.push_str(&format!(" - {}", required_arg.desc)); - } description.push('\n'); + }; + for required_arg in signature.required_positional { + write_arg(required_arg, false); } - for optional_arg in &signature.optional_positional { - if first { - first = false; - } else { - description.push('\n'); - } - description.push_str(&format!( - " `{}: {}`", - optional_arg.name, - optional_arg.shape.to_type() - )); - if !optional_arg.desc.is_empty() { - description.push_str(&format!(" - {}", optional_arg.desc)); - } - description.push('\n'); + for optional_arg in signature.optional_positional { + write_arg(optional_arg, true); } - if let Some(arg) = &signature.rest_positional { + if let Some(arg) = signature.rest_positional { if !first { description.push('\n'); } - description.push_str(&format!(" `...{}: {}`", arg.name, arg.shape.to_type())); - if !arg.desc.is_empty() { - description.push_str(&format!(" - {}", arg.desc)); - } + description.push_str(&format!(" `...{}`", arg.name)); + description.push_str(&doc_for_arg( + Some(arg.shape), + arg.desc, + arg.default_value, + false, + )); description.push('\n'); } description.push('\n'); @@ -378,7 +365,7 @@ mod hover_tests { serde_json::json!({ "contents": { "kind": "markdown", - "value": "Concatenate multiple strings into a single string, with an optional separator between each.\n---\n### Usage \n```nu\n str join {flags} \n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `separator: string` - Optional separator to use when creating string.\n\n\n### Input/output types\n\n```nu\n list | string\n string | string\n\n```\n### Example(s)\n Create a string from input\n```nu\n ['nu', 'shell'] | str join\n```\n Create a string from input with a separator\n```nu\n ['nu', 'shell'] | str join '-'\n```\n" + "value": "Concatenate multiple strings into a single string, with an optional separator between each.\n---\n### Usage \n```nu\n str join {flags} (separator)\n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `separator`: `` - Optional separator to use when creating string. (optional)\n\n\n### Input/output types\n\n```nu\n list | string\n string | string\n\n```\n### Example(s)\n Create a string from input\n```nu\n ['nu', 'shell'] | str join\n```\n Create a string from input with a separator\n```nu\n ['nu', 'shell'] | str join '-'\n```\n" } }) ); diff --git a/crates/nu-lsp/src/notification.rs b/crates/nu-lsp/src/notification.rs index d3c4c8ac3e..32073747b8 100644 --- a/crates/nu-lsp/src/notification.rs +++ b/crates/nu-lsp/src/notification.rs @@ -162,7 +162,7 @@ mod tests { serde_json::json!({ "contents": { "kind": "markdown", - "value": "Create a variable and give it a value.\n\nThis command is a parser keyword. For details, check:\n https://www.nushell.sh/book/thinking_in_nu.html\n---\n### Usage \n```nu\n let {flags} \n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `var_name: any` - Variable name.\n\n `initial_value: any` - Equals sign followed by value.\n\n\n### Input/output types\n\n```nu\n any | nothing\n\n```\n### Example(s)\n Set a variable to a value\n```nu\n let x = 10\n```\n Set a variable to the result of an expression\n```nu\n let x = 10 + 100\n```\n Set a variable based on the condition\n```nu\n let x = if false { -1 } else { 1 }\n```\n" + "value": "Create a variable and give it a value.\n\nThis command is a parser keyword. For details, check:\n https://www.nushell.sh/book/thinking_in_nu.html\n---\n### Usage \n```nu\n let {flags} = \n```\n\n### Flags\n\n `-h`, `--help` - Display the help message for this command\n\n\n### Parameters\n\n `var_name`: `` - Variable name.\n\n `initial_value`: `` - Equals sign followed by value.\n\n\n### Input/output types\n\n```nu\n any | nothing\n\n```\n### Example(s)\n Set a variable to a value\n```nu\n let x = 10\n```\n Set a variable to the result of an expression\n```nu\n let x = 10 + 100\n```\n Set a variable based on the condition\n```nu\n let x = if false { -1 } else { 1 }\n```\n" } }) ); diff --git a/crates/nu-lsp/src/signature.rs b/crates/nu-lsp/src/signature.rs index d431531d8c..6264e2736a 100644 --- a/crates/nu-lsp/src/signature.rs +++ b/crates/nu-lsp/src/signature.rs @@ -5,7 +5,7 @@ use lsp_types::{ use nu_protocol::{ ast::{Argument, Call, Expr, Expression, FindMapResult, Traverse}, engine::StateWorkingSet, - PositionalArg, Signature, + Flag, PositionalArg, Signature, SyntaxShape, Value, }; use crate::{uri_to_path, LanguageServer}; @@ -35,34 +35,85 @@ fn find_active_internal_call<'a>( } } -impl LanguageServer { - pub(crate) fn get_signature_label(signature: &Signature) -> String { - let mut label = String::new(); - label.push_str(&format!(" {}", signature.name)); - if !signature.named.is_empty() { - label.push_str(" {flags}"); - } - for required_arg in &signature.required_positional { - label.push_str(&format!(" <{}>", required_arg.name)); - } - for optional_arg in &signature.optional_positional { - let value_info = if let Some(value) = optional_arg - .default_value - .as_ref() - .and_then(|v| v.coerce_str().ok()) - { - format!("={}", value) - } else { - String::new() - }; - label.push_str(&format!(" <{}?{}>", optional_arg.name, value_info)); - } - if let Some(arg) = &signature.rest_positional { - label.push_str(&format!(" <...{}>", arg.name)); - } - label +pub(crate) fn display_flag(flag: &Flag, verbitam: bool) -> String { + let md_backtick = if verbitam { "`" } else { "" }; + let mut text = String::new(); + if let Some(short_flag) = flag.short { + text.push_str(&format!("{md_backtick}-{short_flag}{md_backtick}")); } + if !flag.long.is_empty() { + if flag.short.is_some() { + text.push_str(", "); + } + text.push_str(&format!("{md_backtick}--{}{md_backtick}", flag.long)); + } + text +} +pub(crate) fn doc_for_arg( + syntax_shape: Option, + desc: String, + default_value: Option, + optional: bool, +) -> String { + let mut text = String::new(); + if let Some(mut shape) = syntax_shape { + if let SyntaxShape::Keyword(_, inner_shape) = shape { + shape = *inner_shape; + } + text.push_str(&format!(": `<{}>`", shape)); + } + if !(desc.is_empty() && default_value.is_none()) || optional { + text.push_str(" -") + }; + if !desc.is_empty() { + text.push_str(&format!(" {}", desc)); + }; + if let Some(value) = default_value.as_ref().and_then(|v| v.coerce_str().ok()) { + text.push_str(&format!( + " ({}default: `{value}`)", + if optional { "optional, " } else { "" } + )); + } else if optional { + text.push_str(" (optional)"); + } + text +} + +pub(crate) fn get_signature_label(signature: &Signature, indent: bool) -> String { + let expand_keyword = |arg: &PositionalArg, optional: bool| match &arg.shape { + SyntaxShape::Keyword(kwd, _) => { + format!("{} <{}>", String::from_utf8_lossy(kwd), arg.name) + } + _ => { + if optional { + arg.name.clone() + } else { + format!("<{}>", arg.name) + } + } + }; + let mut label = String::new(); + if indent { + label.push_str(" "); + } + label.push_str(&signature.name); + if !signature.named.is_empty() { + label.push_str(" {flags}"); + } + for required_arg in &signature.required_positional { + label.push_str(&format!(" {}", expand_keyword(required_arg, false))); + } + for optional_arg in &signature.optional_positional { + label.push_str(&format!(" ({})", expand_keyword(optional_arg, true))); + } + if let Some(arg) = &signature.rest_positional { + label.push_str(&format!(" ...({})", arg.name)); + } + label +} + +impl LanguageServer { pub(crate) fn get_signature_help( &mut self, params: &SignatureHelpParams, @@ -120,6 +171,7 @@ impl LanguageServer { find_active_internal_call(expr, &working_set, pos_to_search) })?; let active_signature = working_set.get_decl(active_call.decl_id).signature(); + let label = get_signature_label(&active_signature, false); let mut param_num_before_pos = 0; for arg in active_call.arguments.iter() { @@ -133,39 +185,51 @@ impl LanguageServer { break; } } + let str_to_doc = |s: String| { Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, value: s, })) }; - let arg_to_param_info = |arg: &PositionalArg| ParameterInformation { - label: lsp_types::ParameterLabel::Simple(arg.name.to_owned()), - documentation: str_to_doc(format!( - ": `<{}>` - {}", - arg.shape.to_type(), - arg.desc.to_owned() + let arg_to_param_info = |arg: PositionalArg, optional: bool| ParameterInformation { + label: lsp_types::ParameterLabel::Simple(arg.name), + documentation: str_to_doc(doc_for_arg( + Some(arg.shape), + arg.desc, + arg.default_value, + optional, )), }; + let flag_to_param_info = |flag: Flag| ParameterInformation { + label: lsp_types::ParameterLabel::Simple(display_flag(&flag, false)), + documentation: str_to_doc(doc_for_arg(flag.arg, flag.desc, flag.default_value, false)), + }; + + // positional args let mut parameters: Vec = active_signature .required_positional - .iter() - .map(arg_to_param_info) + .into_iter() + .map(|arg| arg_to_param_info(arg, false)) .chain( active_signature .optional_positional - .iter() - .map(arg_to_param_info), + .into_iter() + .map(|arg| arg_to_param_info(arg, true)), ) .collect(); - if let Some(rest_arg) = &active_signature.rest_positional { - parameters.push(arg_to_param_info(rest_arg)); + if let Some(rest_arg) = active_signature.rest_positional { + parameters.push(arg_to_param_info(rest_arg, false)); } + let max_idx = parameters.len().saturating_sub(1) as u32; let active_parameter = Some(param_num_before_pos.min(max_idx)); + // also include flags in the end, just for documentation + parameters.extend(active_signature.named.into_iter().map(flag_to_param_info)); + Some(SignatureHelp { signatures: vec![SignatureInformation { - label: Self::get_signature_label(&active_signature), + label, documentation: str_to_doc(active_signature.description), parameters: Some(parameters), active_parameter, @@ -233,7 +297,7 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " str substring {flags} <...rest>", + "label": "str substring {flags} ...(rest)", "parameters": [ ], "activeParameter": 0 }], @@ -263,7 +327,7 @@ mod tests { assert_json_include!( actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " str substring {flags} <...rest>", + "label": "str substring {flags} ...(rest)", "activeParameter": 1 }]}) ); @@ -272,7 +336,7 @@ mod tests { assert_json_include!( actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " str substring {flags} <...rest>", + "label": "str substring {flags} ...(rest)", "activeParameter": 0 }]}) ); @@ -281,7 +345,7 @@ mod tests { assert_json_include!( actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " echo {flags} <...rest>", + "label": "echo {flags} ...(rest)", "activeParameter": 0 }]}) ); @@ -291,8 +355,8 @@ mod tests { fn signature_help_on_custom_commands() { let config_str = r#"export def "foo bar" [ p1: int - p2: string, - p3?: int = 1 # doc + p2: string, # doc + p3?: int = 1 ] {}"#; let (client_connection, _recv) = initialize_language_server(Some(config_str), None); @@ -308,11 +372,11 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " foo bar {flags} ", + "label": "foo bar {flags} (p3)", "parameters": [ - {"label": "p1", "documentation": {"value": ": `` - "}}, - {"label": "p2", "documentation": {"value": ": `` - "}}, - {"label": "p3", "documentation": {"value": ": `` - doc"}}, + {"label": "p1", "documentation": {"value": ": ``"}}, + {"label": "p2", "documentation": {"value": ": `` - doc"}}, + {"label": "p3", "documentation": {"value": ": `` - (optional, default: `1`)"}}, ], "activeParameter": 1 }], @@ -326,11 +390,12 @@ mod tests { actual: result_from_message(resp), expected: serde_json::json!({ "signatures": [{ - "label": " foo baz {flags} ", + "label": "foo baz {flags} (p3)", "parameters": [ - {"label": "p1", "documentation": {"value": ": `` - "}}, - {"label": "p2", "documentation": {"value": ": `` - "}}, - {"label": "p3", "documentation": {"value": ": `` - doc"}}, + {"label": "p1", "documentation": {"value": ": ``"}}, + {"label": "p2", "documentation": {"value": ": `` - doc"}}, + {"label": "p3", "documentation": {"value": ": `` - (optional, default: `1`)"}}, + {"label": "-h, --help", "documentation": {"value": " - Display the help message for this command"}}, ], "activeParameter": 2 }], diff --git a/tests/fixtures/lsp/hints/signature.nu b/tests/fixtures/lsp/hints/signature.nu index 65ee6732f3..da2aba7b7a 100644 --- a/tests/fixtures/lsp/hints/signature.nu +++ b/tests/fixtures/lsp/hints/signature.nu @@ -11,7 +11,7 @@ foo bar 1 2 3 foo baz 1 2 3 def "foo baz" [ p1: int - p2: string, - p3?: int = 1 # doc + p2: string, # doc + p3?: int = 1 ] {} echo From e82df7c1c9766a4c6b43a80db85d8b91c34baa3e Mon Sep 17 00:00:00 2001 From: Douglas <32344964+NotTheDr01ds@users.noreply.github.com> Date: Mon, 7 Apr 2025 00:38:17 -0400 Subject: [PATCH 10/13] Reminder comment to update doc when adding `$nu` constants (#15481) # Description As requested in review on https://github.com/nushell/nushell.github.io/pull/1860 - This adds a reminder comment requesting that contributors update that doc page when adding new constants. # User-Facing Changes None # Tests + Formatting Comment-only # After Submitting This PR should only be merged after https://github.com/nushell/nushell.github.io/pull/1860 is merged into the doc. --- crates/nu-protocol/src/eval_const.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 15ad479e19..b387de4120 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -17,6 +17,8 @@ use std::{ }; /// Create a Value for `$nu`. +// Note: When adding new constants to $nu, please update the doc at https://nushell.sh/book/special_variables.html +// or at least add a TODO/reminder issue in nushell.github.io so we don't lose track of it. pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Value { fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf { #[allow(deprecated)] From 639f4bd4996111b5eb173771d2848615dd142481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Riegel?= <96702577+LoicRiegel@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:25:27 +0200 Subject: [PATCH 11/13] Replace some PipelineMismatch by OnlySupportsThisInputType by shell error (#15447) sub-issue of #10698 according to @sholderbach (Description largely edited, since the scope of the PR changed) # Description Context: `ShellError::OnlySupportsThisInputType` was a duplicate of `ShellError::PipelineMismatch` so I - replaced some occurences of PipelineMismatch by OnlySupportsThisInputType For another PR - replace the remaining occurences - removed OnlySupportsThisInputType from nu-protocol # User-Facing Changes The error message will be different -> but consistent # Tests + Formatting OK # After Submitting Nothing required --- crates/nu-cmd-extra/src/extra/bits/mod.rs | 3 ++- .../src/conversions/split_cell_path.rs | 4 +++- crates/nu-command/src/filters/merge/common.rs | 3 ++- crates/nu-command/src/filters/move_.rs | 3 ++- crates/nu-command/src/filters/sort.rs | 5 +++-- crates/nu-command/src/path/join.rs | 11 ++++++++-- crates/nu-command/src/path/mod.rs | 22 +++++-------------- crates/nu-command/src/strings/parse.rs | 16 +++++++++----- crates/nu-command/src/strings/split/chars.rs | 3 ++- crates/nu-command/src/strings/split/column.rs | 3 ++- crates/nu-command/src/strings/split/row.rs | 3 ++- crates/nu-command/src/strings/split/words.rs | 3 ++- crates/nu-command/src/strings/str_/expand.rs | 4 +++- crates/nu-command/src/strings/str_/stats.rs | 4 +++- 14 files changed, 51 insertions(+), 36 deletions(-) diff --git a/crates/nu-cmd-extra/src/extra/bits/mod.rs b/crates/nu-cmd-extra/src/extra/bits/mod.rs index cbac833896..d64ad614c9 100644 --- a/crates/nu-cmd-extra/src/extra/bits/mod.rs +++ b/crates/nu-cmd-extra/src/extra/bits/mod.rs @@ -159,9 +159,10 @@ where } (Value::Binary { .. }, Value::Int { .. }) | (Value::Int { .. }, Value::Binary { .. }) => { Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "input, and argument, to be both int or both binary" .to_string(), + wrong_type: "int and binary".to_string(), dst_span: rhs.span(), src_span: span, }, diff --git a/crates/nu-command/src/conversions/split_cell_path.rs b/crates/nu-command/src/conversions/split_cell_path.rs index 4ff5bff67c..4854bd9d03 100644 --- a/crates/nu-command/src/conversions/split_cell_path.rs +++ b/crates/nu-command/src/conversions/split_cell_path.rs @@ -40,6 +40,7 @@ impl Command for SplitCellPath { input: PipelineData, ) -> Result { let head = call.head; + let input_type = input.get_type(); let src_span = match input { // Early return on correct type and empty pipeline @@ -54,8 +55,9 @@ impl Command for SplitCellPath { PipelineData::ListStream(stream, ..) => stream.span(), PipelineData::ByteStream(stream, ..) => stream.span(), }; - Err(ShellError::PipelineMismatch { + Err(ShellError::OnlySupportsThisInputType { exp_input_type: "cell-path".into(), + wrong_type: input_type.to_string(), dst_span: head, src_span, }) diff --git a/crates/nu-command/src/filters/merge/common.rs b/crates/nu-command/src/filters/merge/common.rs index 219250d113..849e649d56 100644 --- a/crates/nu-command/src/filters/merge/common.rs +++ b/crates/nu-command/src/filters/merge/common.rs @@ -42,8 +42,9 @@ pub(crate) fn typecheck_merge(lhs: &Value, rhs: &Value, head: Span) -> Result<() match (lhs.get_type(), rhs.get_type()) { (Type::Record { .. }, Type::Record { .. }) => Ok(()), (_, _) if is_list_of_records(lhs) && is_list_of_records(rhs) => Ok(()), - _ => Err(ShellError::PipelineMismatch { + other => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "input and argument to be both record or both table".to_string(), + wrong_type: format!("{} and {}", other.0, other.1).to_string(), dst_span: head, src_span: lhs.span(), }), diff --git a/crates/nu-command/src/filters/move_.rs b/crates/nu-command/src/filters/move_.rs index 48dd24b178..11a165ab9c 100644 --- a/crates/nu-command/src/filters/move_.rs +++ b/crates/nu-command/src/filters/move_.rs @@ -174,8 +174,9 @@ impl Command for Move { PipelineData::Value(Value::Record { val, .. }, ..) => { Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data()) } - _ => Err(ShellError::PipelineMismatch { + other => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "record or table".to_string(), + wrong_type: other.get_type().to_string(), dst_span: head, src_span: Span::new(head.start, head.start), }), diff --git a/crates/nu-command/src/filters/sort.rs b/crates/nu-command/src/filters/sort.rs index 8b8c2e6d60..7eb37d5837 100644 --- a/crates/nu-command/src/filters/sort.rs +++ b/crates/nu-command/src/filters/sort.rs @@ -184,9 +184,10 @@ impl Command for Sort { dst_span: value.span(), }) } - _ => { - return Err(ShellError::PipelineMismatch { + ref other => { + return Err(ShellError::OnlySupportsThisInputType { exp_input_type: "record or list".to_string(), + wrong_type: other.get_type().to_string(), dst_span: call.head, src_span: value.span(), }) diff --git a/crates/nu-command/src/path/join.rs b/crates/nu-command/src/path/join.rs index c36b51fb99..fdf58e3afe 100644 --- a/crates/nu-command/src/path/join.rs +++ b/crates/nu-command/src/path/join.rs @@ -221,14 +221,21 @@ fn join_list(parts: &[Value], head: Span, span: Span, args: &Arguments) -> Value Value::list(vals, span) } - Err(_) => Value::error( - ShellError::PipelineMismatch { + Err(ShellError::CantConvert { from_type, .. }) => Value::error( + ShellError::OnlySupportsThisInputType { exp_input_type: "string or record".into(), + wrong_type: from_type, dst_span: head, src_span: span, }, span, ), + Err(_) => Value::error( + ShellError::NushellFailed { + msg: "failed to join path".into(), + }, + span, + ), } } } diff --git a/crates/nu-command/src/path/mod.rs b/crates/nu-command/src/path/mod.rs index d6f3a19528..030b6eb658 100644 --- a/crates/nu-command/src/path/mod.rs +++ b/crates/nu-command/src/path/mod.rs @@ -51,21 +51,11 @@ fn handle_invalid_values(rest: Value, name: Span) -> Value { fn err_from_value(rest: &Value, name: Span) -> ShellError { match rest { Value::Error { error, .. } => *error.clone(), - _ => { - if rest.is_nothing() { - ShellError::OnlySupportsThisInputType { - exp_input_type: "string, record or list".into(), - wrong_type: "nothing".into(), - dst_span: name, - src_span: rest.span(), - } - } else { - ShellError::PipelineMismatch { - exp_input_type: "string, row or list".into(), - dst_span: name, - src_span: rest.span(), - } - } - } + _ => ShellError::OnlySupportsThisInputType { + exp_input_type: "string, record or list".into(), + wrong_type: rest.get_type().to_string(), + dst_span: name, + src_span: rest.span(), + }, } } diff --git a/crates/nu-command/src/strings/parse.rs b/crates/nu-command/src/strings/parse.rs index b7e546007a..23026e5812 100644 --- a/crates/nu-command/src/strings/parse.rs +++ b/crates/nu-command/src/strings/parse.rs @@ -181,11 +181,14 @@ fn operate( Value::List { vals, .. } => { let iter = vals.into_iter().map(move |val| { let span = val.span(); - val.into_string().map_err(|_| ShellError::PipelineMismatch { - exp_input_type: "string".into(), - dst_span: head, - src_span: span, - }) + let type_ = val.get_type(); + val.into_string() + .map_err(|_| ShellError::OnlySupportsThisInputType { + exp_input_type: "string".into(), + wrong_type: type_.to_string(), + dst_span: head, + src_span: span, + }) }); let iter = ParseIter { @@ -199,8 +202,9 @@ fn operate( Ok(ListStream::new(iter, head, Signals::empty()).into()) } - value => Err(ShellError::PipelineMismatch { + value => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: value.get_type().to_string(), dst_span: head, src_span: value.span(), }), diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index c40bdffa6e..2f2e71d58e 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -153,8 +153,9 @@ fn split_chars_helper(v: &Value, name: Span, graphemes: bool) -> Value { ) } else { Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: v.get_type().to_string(), dst_span: name, src_span: v_span, }, diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs index 663ffff876..a197aa8e2e 100644 --- a/crates/nu-command/src/strings/split/column.rs +++ b/crates/nu-command/src/strings/split/column.rs @@ -255,8 +255,9 @@ fn split_column_helper( v => { let span = v.span(); vec![Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: v.get_type().to_string(), dst_span: head, src_span: span, }, diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs index d1a16e7c7e..c1fbf12df2 100644 --- a/crates/nu-command/src/strings/split/row.rs +++ b/crates/nu-command/src/strings/split/row.rs @@ -219,8 +219,9 @@ fn split_row_helper(v: &Value, regex: &Regex, max_split: Option, name: Sp } } else { vec![Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: v.get_type().to_string(), dst_span: name, src_span: v_span, }, diff --git a/crates/nu-command/src/strings/split/words.rs b/crates/nu-command/src/strings/split/words.rs index 0aa02b5e93..7a3a525ca7 100644 --- a/crates/nu-command/src/strings/split/words.rs +++ b/crates/nu-command/src/strings/split/words.rs @@ -226,8 +226,9 @@ fn split_words_helper(v: &Value, word_length: Option, span: Span, graphem Value::list(words, v_span) } else { Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: v.get_type().to_string(), dst_span: span, src_span: v_span, }, diff --git a/crates/nu-command/src/strings/str_/expand.rs b/crates/nu-command/src/strings/str_/expand.rs index ab4cb7e67f..e6caa213b5 100644 --- a/crates/nu-command/src/strings/str_/expand.rs +++ b/crates/nu-command/src/strings/str_/expand.rs @@ -237,14 +237,16 @@ fn run( input.map( move |v| { let value_span = v.span(); + let type_ = v.get_type(); match v.coerce_into_string() { Ok(s) => { let contents = if is_path { s.replace('\\', "\\\\") } else { s }; str_expand(&contents, span, value_span) } Err(_) => Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: type_.to_string(), dst_span: span, src_span: value_span, }, diff --git a/crates/nu-command/src/strings/str_/stats.rs b/crates/nu-command/src/strings/str_/stats.rs index bd0bbfdd8f..beebbb6c24 100644 --- a/crates/nu-command/src/strings/str_/stats.rs +++ b/crates/nu-command/src/strings/str_/stats.rs @@ -108,6 +108,7 @@ fn stats( input.map( move |v| { let value_span = v.span(); + let type_ = v.get_type(); // First, obtain the span. If this fails, propagate the error that results. if let Value::Error { error, .. } = v { return Value::error(*error, span); @@ -116,8 +117,9 @@ fn stats( match v.coerce_into_string() { Ok(s) => counter(&s, span), Err(_) => Value::error( - ShellError::PipelineMismatch { + ShellError::OnlySupportsThisInputType { exp_input_type: "string".into(), + wrong_type: type_.to_string(), dst_span: span, src_span: value_span, }, From 0f8f3bcf9ab1ca8d3bf170143c1f32c9c287ce50 Mon Sep 17 00:00:00 2001 From: Stefan Holderbach Date: Mon, 7 Apr 2025 13:36:23 +0200 Subject: [PATCH 12/13] Fix Exbibyte parsing (#15515) Closes #15502 # Description The parsing of Exbibytes used the wrong base unit before converting. # User-Facing Changes `1EiB` etc. will now be parsed correctly # Tests + Formatting (-) --- crates/nu-parser/src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 9c4d2b30c4..6dd56b47e3 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2784,7 +2784,7 @@ pub const FILESIZE_UNIT_GROUPS: &[UnitGroup] = &[ ( Unit::Filesize(FilesizeUnit::EiB), "EIB", - Some((Unit::Filesize(FilesizeUnit::EiB), 1024)), + Some((Unit::Filesize(FilesizeUnit::PiB), 1024)), ), (Unit::Filesize(FilesizeUnit::B), "B", None), ]; From 12a1eefe736853b961afac1e2c0207ff6c136052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Riegel?= <96702577+LoicRiegel@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:44:55 +0200 Subject: [PATCH 13/13] Move human date parsing into new command `date from-human` (#15495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No related issue. Decided in nushell's weekly meeting: see [meeting notes](https://hackmd.io/rA1YecqjRh6I5m8dTq7BHw) # Description Converting a date as a human readable string to a datetime: - currently: using the ``into datetime`` command - after this change: using ``date from-human`` command Also moved the ``--list-human`` flag to the new command. # User-Facing Changes - Users have to use a new command for parsing human readable datetimes. Result: ```nushell ~> date from-human --list ╭────┬───────────────────────────────────┬──────────────╮ │ # │ parseable human datetime examples │ result │ ├────┼───────────────────────────────────┼──────────────┤ │ 0 │ Today 18:30 │ in 6 hours │ │ 1 │ 2022-11-07 13:25:30 │ 2 years ago │ │ 2 │ 15:20 Friday │ in 6 days │ │ 3 │ This Friday 17:00 │ in 6 days │ │ 4 │ 13:25, Next Tuesday │ in 3 days │ │ 5 │ Last Friday at 19:45 │ 16 hours ago │ │ 6 │ In 3 days │ in 2 days │ │ 7 │ In 2 hours │ in 2 hours │ │ 8 │ 10 hours and 5 minutes ago │ 10 hours ago │ │ 9 │ 1 years ago │ a year ago │ │ 10 │ A year ago │ a year ago │ │ 11 │ A month ago │ a month ago │ │ 12 │ A week ago │ a week ago │ │ 13 │ A day ago │ a day ago │ │ 14 │ An hour ago │ an hour ago │ │ 15 │ A minute ago │ a minute ago │ │ 16 │ A second ago │ now │ │ 17 │ Now │ now │ ╰────┴───────────────────────────────────┴──────────────╯ ~> "2 days ago" | date from-human Thu, 3 Apr 2025 12:03:33 +0200 (2 days ago) ~> "2 days ago" | into datetime Error: nu::shell::datetime_parse_error × Unable to parse datetime: [2 days ago]. ╭─[entry #5:1:1] 1 │ "2 days ago" | into datetime · ──────┬───── · ╰── datetime parsing failed ╰──── help: Examples of supported inputs: * "5 pm" * "2020/12/4" * "2020.12.04 22:10 +2" * "2020-04-12 22:10:57 +02:00" * "2020-04-12T22:10:57.213231+02:00" * "Tue, 1 Jul 2003 10:52:37 +0200" ``` # Tests + Formatting Fmt, clippy 🆗 Tests 🆗 > Note: I was able to reactivate one unit test in the ``into datetime`` command # After Submitting Here since the user facing changes are significant, I think we should communicate in the released notes. Otherwise the automatically generated documentation should be enough IMO. --- .../src/conversions/into/datetime.rs | 125 +-------- crates/nu-command/src/date/from_human.rs | 259 ++++++++++++++++++ crates/nu-command/src/date/mod.rs | 2 + crates/nu-command/src/default_context.rs | 1 + 4 files changed, 265 insertions(+), 122 deletions(-) create mode 100644 crates/nu-command/src/date/from_human.rs diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index 1ff2d55522..e503f32653 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -1,6 +1,5 @@ use crate::{generate_strftime_list, parse_date_from_string}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc}; -use human_date_parser::{from_human_time, ParseResult}; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; @@ -98,11 +97,6 @@ impl Command for IntoDatetime { "Show all possible variables for use in --format flag", Some('l'), ) - .switch( - "list-human", - "Show human-readable datetime parsing examples", - Some('n'), - ) .rest( "rest", SyntaxShape::CellPath, @@ -120,8 +114,6 @@ impl Command for IntoDatetime { ) -> Result { if call.has_flag(engine_state, stack, "list")? { Ok(generate_strftime_list(call.head, true).into_pipeline_data()) - } else if call.has_flag(engine_state, stack, "list-human")? { - Ok(list_human_readable_examples(call.head).into_pipeline_data()) } else { let cell_paths = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); @@ -256,21 +248,6 @@ impl Command for IntoDatetime { Span::test_data(), )), }, - Example { - description: "Parsing human readable datetimes", - example: "'Today at 18:30' | into datetime", - result: None, - }, - Example { - description: "Parsing human readable datetimes", - example: "'Last Friday at 19:45' | into datetime", - result: None, - }, - Example { - description: "Parsing human readable datetimes", - example: "'In 5 minutes and 30 seconds' | into datetime", - result: None, - }, ] } } @@ -291,60 +268,9 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { if matches!(input, Value::String { .. }) && dateformat.is_none() { let span = input.span(); if let Ok(input_val) = input.coerce_str() { - 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, Local::now().naive_local()) { - match date { - ParseResult::Date(date) => { - let time = Local::now().time(); - let combined = date.and_time(time); - let local_offset = *Local::now().offset(); - let dt_fixed = - TimeZone::from_local_datetime(&local_offset, &combined) - .single() - .unwrap_or_default(); - return Value::date(dt_fixed, span); - } - ParseResult::DateTime(date) => { - 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(); - let combined = date.and_time(time); - let local_offset = *Local::now().offset(); - let dt_fixed = - TimeZone::from_local_datetime(&local_offset, &combined) - .single() - .unwrap_or_default(); - return Value::date(dt_fixed, span); - } - } - } - } - }; + if let Ok(date) = parse_date_from_string(&input_val, span) { + return Value::date(date, span); + } } } @@ -524,44 +450,6 @@ fn action(input: &Value, args: &Arguments, head: Span) -> Value { } } -fn list_human_readable_examples(span: Span) -> Value { - let examples: Vec = vec![ - "Today 18:30".into(), - "2022-11-07 13:25:30".into(), - "15:20 Friday".into(), - "This Friday 17:00".into(), - "13:25, Next Tuesday".into(), - "Last Friday at 19:45".into(), - "In 3 days".into(), - "In 2 hours".into(), - "10 hours and 5 minutes ago".into(), - "1 years ago".into(), - "A year ago".into(), - "A month ago".into(), - "A week ago".into(), - "A day ago".into(), - "An hour ago".into(), - "A minute ago".into(), - "A second ago".into(), - "Now".into(), - ]; - - let records = examples - .iter() - .map(|s| { - Value::record( - record! { - "parseable human datetime examples" => Value::test_string(s.to_string()), - "result" => action(&Value::test_string(s.to_string()), &Arguments { zone_options: None, format_options: None, cell_paths: None }, span) - }, - span, - ) - }) - .collect::>(); - - Value::list(records, span) -} - #[cfg(test)] mod tests { use super::*; @@ -593,14 +481,7 @@ mod tests { } #[test] - #[ignore] fn takes_a_date_format_without_timezone() { - // Ignoring this test for now because we changed the human-date-parser to use - // the users timezone instead of UTC. We may continue to tweak this behavior. - // Another hacky solution is to set the timezone to UTC in the test, which works - // on MacOS and Linux but hasn't been tested on Windows. Plus it kind of defeats - // the purpose of a "without_timezone" test. - // std::env::set_var("TZ", "UTC"); let date_str = Value::test_string("16.11.1984 8:00 am"); let fmt_options = Some(DatetimeFormat("%d.%m.%Y %H:%M %P".to_string())); let args = Arguments { diff --git a/crates/nu-command/src/date/from_human.rs b/crates/nu-command/src/date/from_human.rs new file mode 100644 index 0000000000..f924b4d6d5 --- /dev/null +++ b/crates/nu-command/src/date/from_human.rs @@ -0,0 +1,259 @@ +use chrono::{Local, TimeZone}; +use human_date_parser::{from_human_time, ParseResult}; +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct DateFromHuman; + +impl Command for DateFromHuman { + fn name(&self) -> &str { + "date from-human" + } + + fn signature(&self) -> Signature { + Signature::build("date from-human") + .input_output_types(vec![ + (Type::String, Type::Date), + (Type::Nothing, Type::table()), + ]) + .allow_variants_without_examples(true) + .switch( + "list", + "Show human-readable datetime parsing examples", + Some('l'), + ) + .category(Category::Date) + } + + fn description(&self) -> &str { + "Convert a human readable datetime string to a datetime." + } + + fn search_terms(&self) -> Vec<&str> { + vec![ + "relative", + "now", + "today", + "tomorrow", + "yesterday", + "weekday", + "weekday_name", + "timezone", + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + if call.has_flag(engine_state, stack, "list")? { + return Ok(list_human_readable_examples(call.head).into_pipeline_data()); + } + let head = call.head; + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map(move |value| helper(value, head), engine_state.signals()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parsing human readable datetime", + example: "'Today at 18:30' | date from-human", + result: None, + }, + Example { + description: "Parsing human readable datetime", + example: "'Last Friday at 19:45' | date from-human", + result: None, + }, + Example { + description: "Parsing human readable datetime", + example: "'In 5 minutes and 30 seconds' | date from-human", + result: None, + }, + Example { + description: "PShow human-readable datetime parsing examples", + example: "date from-human --list", + result: None, + }, + ] + } +} + +fn helper(value: Value, head: Span) -> Value { + let span = value.span(); + let input_val = match value { + Value::String { val, .. } => val, + other => { + return Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".to_string(), + wrong_type: other.get_type().to_string(), + dst_span: head, + src_span: span, + }, + span, + ) + } + }; + + if let Ok(date) = from_human_time(&input_val, Local::now().naive_local()) { + match date { + ParseResult::Date(date) => { + let time = Local::now().time(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + return Value::date(dt_fixed, span); + } + ParseResult::DateTime(date) => { + 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(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + return Value::date(dt_fixed, span); + } + } + } + + match from_human_time(&input_val, Local::now().naive_local()) { + Ok(date) => match date { + ParseResult::Date(date) => { + let time = Local::now().time(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + Value::date(dt_fixed, span) + } + ParseResult::DateTime(date) => { + 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, + ); + } + }; + Value::date(dt_fixed, span) + } + ParseResult::Time(time) => { + let date = Local::now().date_naive(); + let combined = date.and_time(time); + let local_offset = *Local::now().offset(); + let dt_fixed = TimeZone::from_local_datetime(&local_offset, &combined) + .single() + .unwrap_or_default(); + Value::date(dt_fixed, span) + } + }, + Err(_) => Value::error( + ShellError::IncorrectValue { + msg: "Cannot parse as humanized date".to_string(), + val_span: head, + call_span: span, + }, + span, + ), + } +} + +fn list_human_readable_examples(span: Span) -> Value { + let examples: Vec = vec![ + "Today 18:30".into(), + "2022-11-07 13:25:30".into(), + "15:20 Friday".into(), + "This Friday 17:00".into(), + "13:25, Next Tuesday".into(), + "Last Friday at 19:45".into(), + "In 3 days".into(), + "In 2 hours".into(), + "10 hours and 5 minutes ago".into(), + "1 years ago".into(), + "A year ago".into(), + "A month ago".into(), + "A week ago".into(), + "A day ago".into(), + "An hour ago".into(), + "A minute ago".into(), + "A second ago".into(), + "Now".into(), + ]; + + let records = examples + .iter() + .map(|s| { + Value::record( + record! { + "parseable human datetime examples" => Value::test_string(s.to_string()), + "result" => helper(Value::test_string(s.to_string()), span), + }, + span, + ) + }) + .collect::>(); + + Value::list(records, span) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(DateFromHuman {}) + } +} diff --git a/crates/nu-command/src/date/mod.rs b/crates/nu-command/src/date/mod.rs index 385420c911..e95acef3dd 100644 --- a/crates/nu-command/src/date/mod.rs +++ b/crates/nu-command/src/date/mod.rs @@ -1,4 +1,5 @@ mod date_; +mod from_human; mod humanize; mod list_timezone; mod now; @@ -7,6 +8,7 @@ mod to_timezone; mod utils; pub use date_::Date; +pub use from_human::DateFromHuman; pub use humanize::DateHumanize; pub use list_timezone::DateListTimezones; pub use now::DateNow; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 0c1b38182a..d4cd7e22f5 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -272,6 +272,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { // Date bind_command! { Date, + DateFromHuman, DateHumanize, DateListTimezones, DateNow,