Reedline,Nu 的行编辑器

Nushell 的行编辑器 ReedlineReedline,Nu 的行编辑器 - 图1 (opens new window) 是一个跨平台的行读取器,它被设计为模块化且颇具灵活性的。该引擎的作用是负责控制命令历史,验证,自动补全,提示以及屏幕绘制。

配置

编辑模式

Reedline 允许你使用两种模式来编辑文本:vi 和 emacs。如果没有指定,默认的编辑模式是 emacs 模式。若要自行设置喜欢的模式,你可以修改配置文件为相应模式。比如:

  1. let $config = {
  2. ...
  3. edit_mode: emacs
  4. ...
  5. }

默认键盘绑定

每种编辑模式都有相应的 vi 或 emacs 文本编辑的常用快捷键设置。

Emacs 和 Vi 快捷键绑定

快捷键事件
EscEsc
Backspace退格
End移至行尾
End补全历史提示
Home移至行首
Ctr + c取消当前行
Ctr + l清除屏幕
Ctr + r搜索历史
Ctr + RightComplete history word
Ctr + Right右移一个词
Ctr + Left左移一个词
Up菜单上移
Up上移
Down菜单下移
Down下移
Left菜单左移
Left左移
Right完成历史提示
Right菜单右移
Right右移
Ctr + b菜单左移
Ctr + b左移
Ctr + f完成历史提示
Ctr + f菜单右移
Ctr + f右移
Ctr + p菜单上移
Ctr + p上移
Ctr + n菜单下移
Ctr + n下移

Vi 普通键绑定

快捷键事件
Ctr + c取消当前行
Ctr + l清除屏幕
Up菜单上移
Up上移
Down菜单下移
Down下移
Left菜单左移
Left左移
Right菜单右移
Right右移

除了之前的键盘绑定,在正常 Vi 模式下,你可以使用经典的 Vi 快捷键来进行移动或者执行相应的动作。可用的组合的选项是:

Vi 正常移动快捷键

快捷键移动
w前移一个单词
d移动到行尾
0移动到行首
$移动到行尾
f行内向右查找字符
t行内右移到指定字符前
F行内向左查找字符
T行内左移到指定字符前

Vi 正常操作快捷键

快捷键操作
d删除
p在光标之后粘贴
P在光标之前粘贴
h左移
l右移
j下移
k上移
w右移一个单词
b左移一个单词
i在光标前插入
a在光标后插入
0移到行首
^移到行首
$移到行尾
u撤销
c修改
x删除字符
s搜索历史
D删除当前位置到行尾
A在当前行最后插入

命令历史

如前所述,Reedline 管理并存储所有被编辑并发送给 Nushell 的命令。要配置 Reedline 可以存储的最大记录数,你需要在配置文件中调整这个值:

  1. let $config = {
  2. ...
  3. max_history_size: 1000
  4. ...
  5. }

定制你的提示

Reedline 的提示语也是高度可定制的。为了构建你的完美提示符,你可以在配置文件中定义下面的环境变量:

  1. # Use nushell functions to define your right and left prompt
  2. def create_left_prompt [] {
  3. let path_segment = ($env.PWD)
  4. $path_segment
  5. }
  6. def create_right_prompt [] {
  7. let time_segment = ([
  8. (date now | date format '%m/%d/%Y %r')
  9. ] | str collect)
  10. $time_segment
  11. }
  12. let-env PROMPT_COMMAND = { create_left_prompt }
  13. let-env PROMPT_COMMAND_RIGHT = { create_right_prompt }

TIP

你并非必须要用 Nushell 的函数来定义环境变量,也可以使用简单的字符串来定义它们。

你也可以通过修改以下环境变量来定制行编辑器的提示符:

  1. let-env PROMPT_INDICATOR = "〉"
  2. let-env PROMPT_INDICATOR_VI_INSERT = ": "
  3. let-env PROMPT_INDICATOR_VI_NORMAL = "〉"
  4. let-env PROMPT_MULTILINE_INDICATOR = "::: "

TIP

提示符是环境变量,它代表了提示的状态

按键绑定

Reedline 按键绑定是一个强大的结构,它允许你建立一连串的事件,而且这些事件可以通过特定的按键组合来触发。

例如,我们假设你想把补全菜单绑定到 Ctrl + t 这组快捷键上(默认是tab)。你可以添加下面的条目到你的配置文件:

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: completion_menu
  6. modifier: control
  7. keycode: char_t
  8. mode: emacs
  9. event: { send: menu name: completion_menu }
  10. }
  11. ]
  12. ...
  13. }

在加载这个新的 config.nu 之后,你的新键盘绑定(Ctrl + t)将打开命令自动补全。

每个按键绑定都需要以下元素:

  • name: 为你的绑定键取一个独特的名字,以便于在$config.keybindings中引用
  • modifier: 绑定键的修饰符。选项有:
    • none
    • control
    • alt
    • shift
    • control | alt
    • control | alt | shift
  • keycode: 这代表要按下的键
  • mode: emacs, vi_insert, vi_normal (一个单独的字符串或一个列表,例如: [vi_insert vi_normal])
  • event: 键盘绑定要发送的事件的类型。选项包括:
    • send
    • edit
    • until

TIP

所有可用的修饰键、键码和事件都可以通过keybindings list命令找到。

TIP

添加到 vi_insert 模式中的按键绑定将在行编辑器处于插入模式(可以写入文本)时可用,而标有 vi_normal 模式的按键绑定将在正常模式下(当光标使用 h、j、k 或 l 移动时)可用。

键盘绑定条目的事件部分是定义要执行的动作的地方。在这个字段,你可以使用一个记录或一个记录列表。比如这样:

  1. ...
  2. event: { send: Enter }
  3. ...

或者

  1. ...
  2. event: [
  3. { edit: Clear }
  4. { send: Enter }
  5. ]
  6. ...

上述第一个按键绑定例子遵循第一种情况,只有一个事件被发送到引擎。

后一个按键绑定的例子是向引擎发送一系列的事件。它首先清除提示,插入一个字符串,然后输入该值。

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: change_dir_with_fzf
  6. modifier: CONTROL
  7. keycode: Char_t
  8. mode: emacs
  9. event:[
  10. { edit: Clear }
  11. { edit: InsertString,
  12. value: "cd (ls | where type == dir | each { |it| $it.name} | str collect (char nl) | fzf | decode utf-8 | str trim)"
  13. }
  14. { send: Enter }
  15. ]
  16. }
  17. ...
  18. }

上面按键绑定的缺点是,插入的文本将被验证处理并保存在历史记录中,这使得按键绑定的执行速度有点慢,而且会用相同的命令来填充命令历史。出于这个原因,可以采用 ExecuteHostCommand 类型的事件。下一个例子以更简单的方式做了与前一个相同的事情,发送了一个单一的事件给引擎:

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: change_dir_with_fzf
  6. modifier: CONTROL
  7. keycode: Char_y
  8. mode: emacs
  9. event: {
  10. send: ExecuteHostCommand,
  11. cmd: "cd (ls | where type == dir | each { |it| $it.name} | str collect (char nl) | fzf | decode utf-8 | str trim)"
  12. }
  13. }
  14. ]
  15. ...
  16. }

在我们继续之前,你一定已经注意到,编辑和发送的语法发生了变化,因此有必要对它们进行更多的解释。 send 是所有可以被引擎处理的 Reedline 事件,而 edit 是所有可以被引擎处理的 EditCommands

send类型

要找到 send 的所有可用选项,你可以使用:

  1. keybindings list | where type == events

send 事件的语法如下:

  1. ...
  2. event: { send: <NAME OF EVENT FROM LIST> }
  3. ...

TIP

你可以用大写字母来命名事件的名称,键盘绑定解析器是不区分大小写的。

这条规则有两个例外:MenuExecuteHostCommand。这两个事件需要一个额外的字段来完成,Menu 需要有一个菜单的名称才能触发(自动补全菜单或历史命令菜单):

  1. ...
  2. event: {
  3. send: menu
  4. name: completion_menu
  5. }
  6. ...

ExecuteHostCommand 需要一个有效的命令,它将被发送到引擎:

  1. ...
  2. event: {
  3. send: ExecuteHostCommand
  4. cmd: "cd ~"
  5. }
  6. ...

值得一提的是,在事件列表中,你还会看到Edit([])Multiple([])UntilFound([])。这些选项对解析器是不可见的,因为它们是基于键盘绑定的定义来构建的。例如,当你在键盘绑定事件里面定义了一个记录列表时,就会为你建立一个Multiple([])事件。Edit([])事件与前面提到的edit类型相同。UntilFound([])事件和前面提到的until类型一样。

edit类型

edit类型是Edit([])事件的简化。event类型简化了定义复杂编辑事件的按键绑定。要列出可用的选项,你可以使用下面的命令:

  1. keybindings list | where type == edits

以下是编辑的常用语法:

  1. ...
  2. event: { edit: <NAME OF EDIT FROM LIST> }
  3. ...

列表中带有 () 的编辑的语法有一点变化,因为这些编辑需要一个额外的值来进行完全定义。例如,如果我们想在提示符所在的位置插入一个字符串,那么你将不得不使用如下方式:

  1. ...
  2. event: {
  3. edit: InsertString
  4. value: "MY NEW STRING"
  5. }
  6. ...

或者说你想向右移动,直到第一个S

  1. ...
  2. event: {
  3. edit: MoveRightUntil
  4. value: "S"
  5. }
  6. ...

正如你所看到的,这两种类型将允许你构建你需要的任何类型的按键绑定。

until类型

为了完成这个按键绑定之旅,我们需要讨论事件的until类型。正如你到目前为止所看到的,你可以发送一个单一的事件或一个事件列表。而当一个事件列表被发送时,每一个事件都会被处理。

然而,在有些情况下,你可能想把不同的事件分配给同一个键盘绑定。这在 Nushell 菜单中特别有用。例如,假设你仍然想用Ctrl + t激活你的补全菜单,但你也想在菜单被激活后用同一个快捷键移动到下一个元素。

对于这些情况,我们有until关键字。在until事件中列出的事件将被逐一处理,不同的是,一旦一个事件被成功处理,事件处理就会停止。

下一个键盘绑定就是这种情况:

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: completion_menu
  6. modifier: control
  7. keycode: char_t
  8. mode: emacs
  9. event: {
  10. until: [
  11. { send: menu name: completion_menu }
  12. { send: MenuNext }
  13. ]
  14. }
  15. }
  16. ]
  17. ...
  18. }

上面的按键绑定将首先尝试打开一个补全菜单。如果菜单没有激活,它将激活它并发送一个成功信号。如果再次按下按键绑定,因为已经有一个激活的菜单,那么它将发送的下一个事件是MenuNext,这意味着它将把选择器移动到菜单的下一个元素。

正如你所看到的,until关键字允许我们为同一个键盘绑定定义两个事件。在写这篇文章的时候,只有菜单事件允许这种类型的分层。其他非菜单事件类型将总是返回一个成功值,这意味着until事件在到达第一个命令时就会停止。

例如,下一个按键绑定将总是发送一个down,因为该事件总是成功的。

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: completion_menu
  6. modifier: control
  7. keycode: char_t
  8. mode: emacs
  9. event: {
  10. until: [
  11. { send: down }
  12. { send: menu name: completion_menu }
  13. { send: menunext }
  14. ]
  15. }
  16. }
  17. ]
  18. ...
  19. }

移除一个默认的按键绑定

如果你想删除某个默认的按键绑定,而不打算使用不同的动作来替代它,你可以设置event: null

例如,在所有的编辑模式下,禁用 Ctrl + l 清除屏幕:

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. modifier: control
  6. keycode: char_l
  7. mode: [emacs, vi_normal, vi_insert]
  8. event: null
  9. }
  10. ]
  11. ...
  12. }

排查键盘绑定问题

你的终端环境可能并不总是以你期望的方式将你的组合键冒泡到 Nushell 上。 你可以使用keybindings listen命令来确定 Nushell 是否真的收到了某些按键,以及如何收到的。

菜单

感谢 Reedline,Nushell 的菜单可以帮助你完成日常的 Shell 脚本操作。接下来我们介绍一下在使用 Nushell 时一直可用的默认菜单。

帮助菜单

帮助菜单的存在是为了方便你过渡到 Nushell。假设你正在组建一个惊人的管道,然后你忘记了反转一个字符串的内部命令。你可以用ctr+q来激活帮助菜单,而不是删除你的管道。一旦激活,只需输入你要找的命令的关键词,菜单就会显示与你的输入相匹配的命令,而匹配的依据就是命令的名称或描述。

要浏览菜单,你可以用tab选择下一个元素,你可以按左键或右键滚动描述,你甚至可以在行中粘贴可用的命令例子。

帮助菜单可以通过修改以下参数进行配置:

  1. let $config = {
  2. ...
  3. menus = [
  4. ...
  5. {
  6. name: help_menu
  7. only_buffer_difference: true # Search is done on the text written after activating the menu
  8. marker: "? " # Indicator that appears with the menu is active
  9. type: {
  10. layout: description # Type of menu
  11. columns: 4 # Number of columns where the options are displayed
  12. col_width: 20 # Optional value. If missing all the screen width is used to calculate column width
  13. col_padding: 2 # Padding between columns
  14. selection_rows: 4 # Number of rows allowed to display found options
  15. description_rows: 10 # Number of rows allowed to display command description
  16. }
  17. style: {
  18. text: green # Text style
  19. selected_text: green_reverse # Text style for selected option
  20. description_text: yellow # Text style for description
  21. }
  22. }
  23. ...
  24. ]
  25. ...

补全菜单

补全菜单是一个上下文敏感的菜单,它将根据提示的状态给出建议。这些建议的范围包括从路径建议到替代命令。在编写命令时,你可以激活该菜单以查看内部命令的可用选项。另外,如果你已经为外部命令定义了你的自定义补全方式,这些补全提示也会出现在菜单中。

默认情况下,补全菜单是通过按tab访问的,它可以通过修改配置对象中的这些值来进行配置:

  1. let $config = {
  2. ...
  3. menus = [
  4. ...
  5. {
  6. name: completion_menu
  7. only_buffer_difference: false # Search is done on the text written after activating the menu
  8. marker: "| " # Indicator that appears with the menu is active
  9. type: {
  10. layout: columnar # Type of menu
  11. columns: 4 # Number of columns where the options are displayed
  12. col_width: 20 # Optional value. If missing all the screen width is used to calculate column width
  13. col_padding: 2 # Padding between columns
  14. }
  15. style: {
  16. text: green # Text style
  17. selected_text: green_reverse # Text style for selected option
  18. description_text: yellow # Text style for description
  19. }
  20. }
  21. ...
  22. ]
  23. ...

通过修改这些参数,你可以根据自己的喜好定制你的菜单布局。

历史菜单

历史菜单是访问编辑器命令历史的一个便捷方法。当激活菜单时(默认为Ctrl+x),命令的历史会以时间倒序显示,这使得选择前一个命令变得非常容易。

历史菜单可以通过修改配置对象中的这些值进行配置:

  1. let $config = {
  2. ...
  3. menus = [
  4. ...
  5. {
  6. name: help_menu
  7. only_buffer_difference: true # Search is done on the text written after activating the menu
  8. marker: "? " # Indicator that appears with the menu is active
  9. type: {
  10. layout: list # Type of menu
  11. page_size: 10 # Number of entries that will presented when activating the menu
  12. }
  13. style: {
  14. text: green # Text style
  15. selected_text: green_reverse # Text style for selected option
  16. description_text: yellow # Text style for description
  17. }
  18. }
  19. ...
  20. ]
  21. ...

当历史菜单被激活时,它从历史中拉出page_size个记录并在菜单中呈现。如果终端还有空间,当你再次按Ctrl+x时,菜单将拉出相同数量的记录,并将它们追加到当前页。如果不可能呈现所有拉出的记录,菜单将创建一个新的页面。可以通过按Ctrl+z转到上一页或Ctrl+x转到下一页来浏览这些页面。

搜索历史记录

要在你的命令历史中搜索,你可以开始输入你要找的命令的关键词。一旦菜单被激活,你输入的任何内容都会被历史记录中选定的命令所取代。例如,假设你已经输入了以下内容:

  1. let a = ()

你可以把光标放在 () 内并激活菜单,你可以通过输入关键词来过滤历史记录,一旦你选择了一个条目,输入的词就会被替换:

  1. let a = (ls | where size > 10MiB)

菜单快速选择

菜单的另一个很好的特性是能够快速选择其中的内容。假设你已经激活了你的菜单,它看起来像这样:

  1. >
  2. 0: ls | where size > 10MiB
  3. 1: ls | where size > 20MiB
  4. 2: ls | where size > 30MiB
  5. 3: ls | where size > 40MiB

你可以输入!3,然后按回车,而不是按向下键去选择第四个条目。这将在提示位置插入选定的文本,节省你向下滚动菜单的时间。

历史搜索和快速选择可以一起使用。你可以激活菜单,进行快速搜索,然后使用前面的方法进行快速选择。

用户定义菜单

如果你发现默认的菜单对你来说是不够的,你需要要创建自己的菜单,Nushell 也可以帮你做到这点。

为了添加一个满足你需求的新菜单,你可以使用其中一个默认的布局作为模板。Nushell 中可用的模板有列式、列表式或描述式。

列式菜单将以列的方式向你显示数据,并根据你的列中显示的文本大小调整列数。

列表类型的菜单将总是以列表的形式显示建议,你可以通过使用!加数字的组合来选择值。

描述类型将给你更多的空间来显示一些值的描述,以及可以插入到缓冲区的额外信息。

假设我们想创建一个菜单,用于显示在你的会话中创建的所有变量,我们将把它称为vars_menu。这个菜单将使用一个列表布局 (layout: list)。为了搜索值,我们希望只使用菜单激活后输入的东西(only_buffer_difference: true)。

满足这些所需的菜单将看起来像这样:

  1. let $config = {
  2. ...
  3. menus = [
  4. ...
  5. {
  6. name: vars_menu
  7. only_buffer_difference: true
  8. marker: "# "
  9. type: {
  10. layout: list
  11. page_size: 10
  12. }
  13. style: {
  14. text: green
  15. selected_text: green_reverse
  16. description_text: yellow
  17. }
  18. source: { |buffer, position|
  19. $nu.scope.vars
  20. | where name =~ $buffer
  21. | sort-by name
  22. | each { |it| {value: $it.name description: $it.type} }
  23. }
  24. }
  25. ...
  26. ]
  27. ...

正如你所看到的,新的菜单与之前描述的history_menu是相同的,唯一的区别是新的字段叫sourcesource字段是 Nushell 所定义的,它包含了你想在菜单中显示的值。对于这个菜单,我们从$nu.scope.vars中提取数据,然后用它来创建记录并填充菜单。

记录所需的结构如下:

  1. {
  2. value: # The value that will be inserted in the buffer
  3. description: # Optional. Description that will be display with the selected value
  4. span: { # Optional. Span indicating what section of the string will be replaced by the value
  5. start:
  6. end:
  7. }
  8. extra: [string] # Optional. A list of strings that will be displayed with the selected value. Only works with a description menu
  9. }

为了让菜单显示一些东西,至少value字段必须存在于结果记录中。

为了使菜单具有交互性,这两个变量在块中可用:$buffer$position$buffer包含菜单捕获的值,当选项only_buffer_difference为真时,$buffer是菜单被激活后输入的文本。如果only_buffer_difference是假的,$buffer是行中所有的字符串。$position变量可以用来根据你对菜单的设想创建替换范围。$position的值会随着only_buffer_difference是真还是假而改变。当为真时,$position是在菜单激活后插入文本的字符串的起始位置;当值为 false 时,$position表示实际的光标位置。

利用这些信息,你可以设计你的菜单来呈现你所需要的信息,并在需要的位置替换该值。之后,玩转你的菜单唯一额外需要做的事情是定义一个按键绑定,并用于激活你的全新菜单。

菜单按键绑定

如果你想改变两个菜单的默认激活方式,可以通过定义新的按键绑定来实现。例如,接下来的两个按键绑定设置分别将Ctrl+tCtrl+y定义为触发自动补全和历史菜单:

  1. let $config = {
  2. ...
  3. keybindings: [
  4. {
  5. name: completion_menu
  6. modifier: control
  7. keycode: char_t
  8. mode: [vi_insert vi_normal]
  9. event: {
  10. until: [
  11. { send: menu name: completion_menu }
  12. { send: menupagenext }
  13. ]
  14. }
  15. }
  16. {
  17. name: history_menu
  18. modifier: control
  19. keycode: char_y
  20. mode: [vi_insert vi_normal]
  21. event: {
  22. until: [
  23. { send: menu name: history_menu }
  24. { send: menupagenext }
  25. ]
  26. }
  27. }
  28. ]
  29. ...
  30. }