自定义 Zsh Widget 来实现自动注释/反注释功能。
基本命令
查看已有 Widgets: zle -al
。
新增一个 Widget:zle -N <funcName>
。
绑定 Widget:bindkey <keycode> <widgetName>
单行版本
在 .zshrc
中加入下列代码:
autocomment() {
local CONTENT="$BUFFER"
if [[ ${#CONTENT} = "0" ]]; then
return;
fi
if [[ "$CONTENT" = "#"* ]]; then
CURSOR=$((CURSOR - 1))
BUFFER="${CONTENT:1}"
else
CURSOR=$((CURSOR + 1)) # 移动光标位置
BUFFER="#$BUFFER"
fi
}
zle -N autocomment
bindkey '^_' autocomment
CURSOR 变量的限制
CURSOR
变量什么时候赋值是有讲究的。
根据 Zle 文档 的说法:
CURSOR (integer)
(…) This is in the range 0 to$#BUFFER
(…) Attempts to move the cursor outside the buffer will result in the cursor being moved to the appropriate end of the buffer.
也就是说,给 CURSOR 赋的值不能超过 $#BUFFER
。
可以简单验证一下:
testCursor() {
echo "\nOriginal C: $CURSOR"
BUFFER="a"
echo "Change Buffer C: $CURSOR"
CURSOR=$((CURSOR+10))
echo "Force edit C: $CURSOR"
CURSOR=100
echo "Force assign C: $CURSOR"
BUFFER="ab"
echo "Change Buffer C: $CURSOR"
CURSOR=$((CURSOR+10))
echo "Force edit C: $CURSOR"
CURSOR=100
echo "Force assign C: $CURSOR"
}
zle -N testCursor
bindkey '^_' testCursor
其结果是:
可以看到,CURSOR
变量不能随意赋值。
# ...
if [[ "$CONTENT" = "#"* ]]; then
CURSOR=$((CURSOR - 1))
BUFFER="${CONTENT:1}"
else
# ...
因此对于这一段脚本,假设当前的 CURSOR
已经在文本结尾(等于 $#BUFFER
)。
如果先赋值 BUFFER
再赋值 CURSOR
,那么实际情形是在给 BUFFER
赋值之后,CURSOR
变量已经被减去了 1,再减一次就会出现问题。
同样的,插入注释符号时,也应该先给 BUFFER
赋值再修改 CURSOR
,否则修改会失效。
绑定到组合键
bindkey '^_' autocomment
其中,^_
代表组合键 Ctrl+/
。绑定其他组合键可以使用 read
命令或者按一下 Ctrl+V
(在 bash/zsh 里代表 quoted-insert
),可以查看组合键对应的输出。
注意,如果使用了 zsh-syntax-highlighting
,请按照官方指引,在 .zshrc
结尾处 source 它的脚本,而不是写到 plugins
数组里。
这是因为 z-sy-h
需要 wrap zle widgets 来使 BUFFER 被修改之后能被重新染色。
当然,也可以手动将 _zsh_highlight
注册成 zle,然后在函数结尾通过zle _zsh_highlight
手动调用——但是为什么要这么做呢?
这个版本的 autocomment
有一个问题:只能对单行输入工作。接下来会将其修改成多行版本。
多行版本
autocomment() {
local CONTENT="$BUFFER"
if [[ ${#CONTENT} = "0" ]]; then
return;
fi
local CONTENT_ARRAY
IFS=$'\n' CONTENT_ARRAY=("${(@f)CONTENT}")
local COMMENT_MODE="uncomment"
for ((i = 1; i <= ${#CONTENT_ARRAY[@]}; i++)); do
if [[ ! ${CONTENT_ARRAY[$i]} = "#"* ]]; then
COMMENT_MODE="comment"
break
fi
done
local LINE_NUMBER=1
local CHARS_POS=0
for ((i = 1; i <= ${#CONTENT_ARRAY[@]}; i++)); do
CHARS_POS=$(( CHARS_POS + ${#CONTENT_ARRAY[$i]} + 1 ))
if (( $CHARS_POS <= $CURSOR )); then
LINE_NUMBER=$((LINE_NUMBER + 1))
fi
if [[ $COMMENT_MODE = "comment" ]]; then
CONTENT_ARRAY[$i]="#${CONTENT_ARRAY[$i]}"
else
CONTENT_ARRAY[$i]="${CONTENT_ARRAY[$i]:1}"
fi
done
if [[ $COMMENT_MODE = "uncomment" ]]; then
CURSOR=$((CURSOR - LINE_NUMBER))
fi
BUFFER=$(join_array $'\n' "${CONTENT_ARRAY[@]}")
if [[ $COMMENT_MODE = "comment" ]]; then
CURSOR=$((CURSOR + LINE_NUMBER))
fi
}
多行版本主要存在如下几个要点:
从功能上讲,只有所有行都以
#
开头时才能认为输入是被程序化处理过的注释。只要有一行不是注释,其余所有的已注释行都应该认为是人工输入的注释,而非本脚本生成的。 因此首先需要判别COMMENT_MODE
。由于每行都要插入或删除一个注释符号,通过计算字符长度来判断光标在第几行,并以行号为依据修改光标位置。注意
CHARS_POS
需要额外+1
,代表换行符。
脚本编写的时候需要特别注意的一点是 zsh 中使用 IFS
分割数组的方法,参见 Parameter Expansion Flags。
在 bash
中,可以这样分割数组:
CONTENT="a b c"
IFS=' ' CONTENT_ARRAY=($CONTENT)
echo ${#CONTENT_ARRAY[@]}
而在 zsh
中,需要这样:
CONTENT="a b c"
IFS=' ' CONTENT_ARRAY=("${(@ps:$IFS:)CONTENT}")
echo ${#CONTENT_ARRAY[@]}
其中,@f
是 @ps:\n:
的缩写,上面的脚本中用的就是这个。
join_array
join_array
是取自 shell-utils 的一个函数,具体参见原文。
Reference: