Editors (Vim) · Missing Semester (mit.edu)
Which editor to learn?
程序员们对自己正在使用的文本编辑器通常有着 非常强的执念。
现在最流行的编辑器是什么?Stack Overflow 的调查显示,Visual Studio Code 是目前最流行的代码编辑器。而 Vim 则是最流行的基于命令行的编辑器。
Vim
Vim 有着悠久历史;它始于 1976 年的 Vi 编辑器,到现在还在不断开发中。很多其他工具也支持 Vim 模式(比如,140 万人安装了 Vim emulation for VS code)。
Philosophy of Vim
在编程的时候,你会把大量时间花在阅读/编辑而不是在写代码上。所以,Vim 是一个modal editor多模态编辑器:它对于插入文字和操纵文字有不同的模式。Vim 是可编程的(可以使用 Vimscript 或者像 Python 一样的其他程序语言),Vim 的接口本身也是一个程序语言:键入操作(以及其助记名) 是命令,这些命令也是可组合的。Vim 避免了使用鼠标,因为那样太慢了;Vim 甚至避免用上下左右键因为那样需要太多的手指移动。
这样的设计哲学使得 Vim 成为了一个能跟上你思维速度的编辑器。
Modal editing
Vim 具有多种操作模式:
- Normal,正常模式:在文件中四处移动光标进行修改
- Insert,插入模式:插入文本
- Replace,替换模式:替换文本
- Visual,可视化模式(一般,行,块):选中文本块
- Command-line,命令模式:用于执行命令
在不同的操作模式下,键盘敲击的含义也不同。比如,x
在Insert插入模式会插入字母 x
,但是在Normal正常模式会删除当前光标所在的字母,在Visual可视模式下则会删除选中文块。
通常你会把大部分时间花在正常模式和插入模式。
你可以按下 <ESC>
从任何其他模式返回正常模式。在正常模式,键入 i
进入插入 模式,R
进入替换模式,v
进入可视(一般)模式,V
进入可视(行)模式,<C-v>
(Ctrl-V, 有时也写作 ^V
)进入可视(块)模式,:
进入命令模式。
Basics
Inserting text
在正常模式,键入 i
进入插入模式。
Buffers, tabs, and windows
Vim 会维护一系列打开的文件,称为“缓存”。一个 Vim 会话包含一系列标签页,每个标签页包含一系列窗口(分隔面板)。每个窗口显示一个缓存。跟网页浏览器等其他你熟悉的程序不一样的是, 缓存和窗口不是一一对应的关系;窗口只是视角。一个缓存可以在多个窗口打开,甚至在同一个标签页内的多个窗口打开。这个功能其实很好用,比如在查看同一个文件的不同部分的时候。
Vim 默认打开一个标签页,这个标签也包含一个窗口。
Command-line
在正常模式下键入 :
进入命令行模式。 在键入 :
后,你的光标会立即跳到屏幕下方的命令行。
-
:q
退出(关闭窗口) -
:w
保存(写) -
:wq
保存然后退出 -
:e {文件名}
打开要编辑的文件 -
:ls
显示打开的缓存 -
:help {标题}
打开帮助文档:help :w
打开:w
命令的帮助文档:help w
打开w
移动的帮助文档
Vim’s interface is a programming language,接口
键入操作(以及他们的助记名) 本身是命令,这些命令可以组合使用。这使得移动和编辑更加高效。
Movement
多数时候你会在正常模式下,使用移动命令在缓存中导航。在 Vim 里面移动也被称为 “nouns”, 因为它们指向文字块。
-
基本移动:
hjkl
(左, 下, 上, 右) -
词:
w
(下一个词),b
(词初),e
(词尾) -
行:
0
(行初),^
(第一个非空格字符),$
(行尾) -
屏幕:
H
(屏幕首行),M
(屏幕中间),L
(屏幕底部) -
翻页:
Ctrl-u
(上翻),Ctrl-d
(下翻) -
文件:
gg
(文件头),G
(文件尾) -
行数:
:{行数}<CR>
回车键或者{行数}G
({行数}为行数) -
杂项:
%
(找到配对,比如括号或者 /* */ 之类的注释对) -
查找:
f{字符}
,t{字符}
,F{字符}
,T{字符}
- 查找/到 向前/向后 在本行的{字符}
,
/;
用于导航匹配
-
搜索:
/{正则表达式}
,n
/N
用于导航匹配
Selection
可视化模式:
- 可视化:
v
- 可视化行:
V
- 可视化块:
Ctrl+v
Edits
Vim 的编辑命令也被称为 “verbs”, 因为动词可以施动于名词。
-
i
进入插入模式- 但是对于操纵/编辑文本,不单想用退格键完成
-
O
/o
在之上/之下插入行 -
d{移动命令}
删除 {移动命令}- 例如,
dw
删除词,d$
删除到行尾,d0
删除到行头。
- 例如,
-
c{移动命令}
改变 {移动命令}- 例如,
cw
改变词 - 比如
d{移动命令}
再i
- 例如,
-
x
删除字符(等同于dl
) -
s
替换字符(等同于cl
) -
可视化模式 + 操作
- 选中文字,
d
删除 或者c
改变
- 选中文字,
-
u
撤销,<C-r>
重做 -
y
复制 / “yank” (其他一些命令比如d
也会复制) -
p
粘贴 -
~
改变字符的大小写
Counts
可以用一个计数来结合“nouns”和“verbs”,这会执行指定操作若干次。
3w
向后移动三个词5j
向下移动5行7dw
删除7个词
Modifiers,修饰语
可以用修饰语改变“noun”的意义。修饰语有 i
,表示“内部”或者“在内”,和 a
, 表示“周围”。
ci(
改变当前括号内的内容ci[
改变当前方括号内的内容da'
删除一个单引号字符串, 包括周围的单引号
Demo
这里是一个有bug的 fizz buzz 实现:
def fizz_buzz(limit):
for i in range(limit):
if i % 3 == 0:
print('fizz')
if i % 5 == 0:
print('fizz')
if i % 3 != 0 and i % 5 != 0:
print(i)
def main():
fizz_buzz(10)
我们会修复以下问题:
- 希望调用主函数
- 希望range从 1 开始
- 希望在 5 的整数倍的时候打印 “buzz”
- 希望在 15 的整数倍的时候在同一行打印 “fizz” 和 “buzz”
- 希望从命令控制行读取参数而不是采用硬编码的参数 10
修改后的代码为:
import sys
def fizz_buzz(limit):
for i in range(1, limit + 1):
if i % 3 == 0:
print('fizz', end='')
if i % 5 == 0:
print('buzz', end='')
if i % 3 != 0 and i % 5 != 0:
print(i, end='')
print()
def main():
fizz_buzz(int(sys.argv[1]))
if __name__== '__main__':
main()
具体修复bug的步骤:
-
希望调用主函数
-
G
文件尾 -
o
向下打开一个新行 -
输入
if name == '__main__':
main()
-
-
希望range从 1 开始
- 搜索
/range
ww
向后移动两个词i
插入文字, “1, “ea
在 limit 后插入, “+1”
- 搜索
-
希望在 5 的整数倍的时候打印 “buzz”
- 搜索
/fizz
n
,移动到下一个fizzci'
更改引号中的内容, 变为buzz
- 搜索
-
希望在 15 的整数倍的时候在同一行打印 “fizz” 和 “buzz”
gg
文件头,jjj
将光标下移,光标在print('fizz')
这行时,$
使光标移动到行尾,i
插入模式,敲入, end=’’
jj.
在print('buzz')
后面同样插入, end=’’
,.
可以重复之前的命令jjo
将光标下移并向下插入一行,敲入print()
,令每个数字的输出换行
-
希望从命令控制行读取参数而不是采用硬编码的参数 10
ggO
文件头,向上插入一行,输入import sys
/10
,定位到参数10处ci(
更改括号中的内容, 变为int(sys.argv[1])
Customizing Vim,自定义
Vim 由一个位于 ~/.vimrc
的文本配置文件(包含 Vim 脚本命令)。
我们提供一个文档详细的基本设置,你可以用它当作你的初始设置。我们推荐使用这个设置因为它修复了一些 Vim 默认设置奇怪行为。 在这儿下载我们的设置,然后将它保存成 ~/.vimrc
.
daisy@Daisy:~$ sudo cp /tmp/vimrc /etc/vim
花时间探索自定义选项是值得的。你可以参考其他人的在 GitHub 上共享的设置文件,比如,你的授课人的 Vim 设置 (Anish, Jon (uses neovim), Jose)。
Extending Vim
Vim 有很多扩展插件。你不需要在 Vim 使用一个插件管理器(从 Vim 8.0 开始)。你可以使用内置的插件管理系统。只需要创建一个 ~/.vim/pack/vendor/start/
的文件夹,然后把插件放到这里(比如通过 git clone
)。
以下是一些我们最爱的插件:
- ctrlp.vim: 模糊文件查找
- ack.vim: 代码搜索
- nerdtree: 文件浏览器
- vim-easymotion: 魔术操作
我安装了nerdtree:
daisy@Daisy:/usr/share/vim/vim82/pack/vendor/start$ sudo git clone --depth 1 https://github.com/preservim/nerdtree.git
daisy@Daisy:~$ vim -u NONE -c "helptags /usr/share/vim/vim82/pack/vendor/start" -c q
vim插件—NERDTree 的安装及简单设置_idea nerdtree 怎么安装-CSDN博客
用vim打开一个文件,输入:NERDTree
你可以查看讲师们的开源的配置文件 (Anish, Jon, Jose) 来看看我们使用的其他插件。 浏览 Vim Awesome 来了解一些很棒的插件。
Vim-mode in other programs
很多工具提供了 Vim 模式。
Shell
如果你是一个 Bash 用户,用 set -o vi
。如果你用 Zsh:bindkey -v
。Fish 用 fish_vi_key_bindings
。另外,不管利用什么 shell,你可以 export EDITOR=vim
。 这是一个用来决定当一个程序需要启动编辑时启动哪个的环境变量。 例如,git
会使用这个编辑器来编辑 commit 信息。
Readline
很多程序使用 GNU Readline 库来作为 它们的命令控制行界面。Readline 也支持基本的 Vim 模式, 可以通过在 ~/.inputrc
添加如下行开启:
set editing-mode vi
比如,在这个设置下,Python REPL 会支持 Vim 快捷键。
Others
甚至有 Vim 的网页浏览快捷键 browsers, 受欢迎的有用于 Google Chrome 的 Vimium 和用于 Firefox 的 Tridactyl。 你甚至可以在 Jupyter notebooks 中用 Vim 快捷键。 这个列表 中列举了支持类 vim 键位绑定的软件。
Advanced Vim
Search and replace
:s
(替换)命令(文档)。
-
%s/foo/bar/g
- 在整个文件中将 foo 全局替换成 bar
-
%s/\[.*\](\(.*\))/\1/g
- 将有命名的 Markdown 链接替换成简单 URLs
-
%s/foo/bar/g
%s
表示执行替换操作,foo
是要被替换的文本,bar
是替换后的文本,而g
表示全局替换。 -
%s/\[.*\](\(.*\))/\1/g
这里的命名链接的格式是[链接文本](URL)
。这个替换命令使用了正则表达式来匹配命名链接的格式。\[.*\]
匹配方括号中的链接文本,\(
和\)
用于捕获URL部分以便在替换中引用,而\1
表示引用捕获的第一个组(即URL部分)。g
表示全局替换。
举例,如果当前行的文本是:“Visit OpenAI for AI research.”, 执行%s/\[.*\](\(.*\))/\1/g
后,文本将变为:“Visit https://openai.com for AI research.”。
Multiple windows
- 用
:sp
/:vsp
来分割窗口 - 同一个缓存可以在多个窗口中显示。
Macros,宏
在Vim中,宏(Macros)可以用于自动化重复性的编辑任务,通过录制一系列按键和编辑命令,然后将其保存为宏,以后可以再次执行这个宏来重放录制的操作序列。
- 录制宏:进入宏录制模式,使用
q
命令后跟一个字符来指定宏的寄存器(例如q
和a
组合成qa
表示录制到a
寄存器),然后执行一系列编辑操作,包括按键、移动光标和执行命令等。 - 停止录制:按下
q
键停止录制宏。 - 回放宏:使用
@
命令后跟宏的寄存器字符来执行宏的回放。例如,@a
将回放寄存器a
中录制的宏操作序列。
Vim宏的灵活性在于可以录制和回放几乎任何编辑操作,包括插入文本、删除、复制、移动光标、执行命令和搜索等。
-
q{字符}
来开始在寄存器{字符}
中录制宏 -
q
停止录制 -
@{字符}
重放宏 -
宏的执行遇错误会停止
-
{计数}@{字符}
执行一个宏{计数}次 -
宏可以递归
- 在Vim中,宏是支持递归的,也就是说,你可以在宏的录制过程中使用
@{character}
来调用当前正在录制的宏,实现宏的递归执行。 - 首先用
q{字符}q
清除宏 - 录制该宏,用
@{字符}
来递归调用该宏 (在录制完成之前不会有任何操作)
- 在Vim中,宏是支持递归的,也就是说,你可以在宏的录制过程中使用
-
例子:将 xml 转成 json (file)
-
一个有 “name” / “email” 键对象的数组
-
可以用一个 Python 程序
-
或者用 sed / 正则表达式
g/people/d
%s/<person>/{/g
%s/<name>\(.*\)<\/name>/"name": "\1",/g
-
g/people/d
这个命令的作用是删除所有包含字符串"people"的行。它用于删除不需要的行,以确保只保留有用的数据行。%s/<person>/{/g
这个命令的作用是将所有出现的<person>
标签替换为左花括号{
。这是将XML转换为JSON时的第一步,将每个<person>
标签作为JSON对象的开始。%s/<name>\(.*\)<\/name>/"name": "\1",/g
这个命令的作用是将所有出现的<name>...</name>
标签替换为相应的JSON键值对。其中,\(...\)
用于捕获<name>
标签中的内容,\1
表示引用捕获的内容。替换后的形式为"name": "捕获的内容"
。
-
或者Vim 命令 / 宏
-
ggdd
,Gdd
删除第一行和最后一行 -
Macro to format a single element(寄存器
e
)-
跳转到有
<name>
的行 -
qe^r"f>s": "<ESC>f<C"<ESC>q
qe
:进入宏录制模式并将宏保存到e
寄存器。^
:将光标移动到行首。r"
:将当前字符替换为"
。f<
:将光标移动到下一个<
字符。s": "<ESC>
:删除当前字符并插入": "
。f<C"
:将光标移动到下一个"
字符。<ESC>
:退出插入模式。q
:停止录制宏。
-
-
Macro to format a person(寄存器
p
)-
跳转到有
<person>
的行 -
qpS{<ESC>j@eA,<ESC>j@ejS},<ESC>q
-
qp
:进入宏录制模式并将宏保存到p
寄存器。 -
S{<ESC>
:删除当前行并插入{
。 -
j@eA,<ESC>
:将光标下移一行并在行尾插入,
。 -
j@ejS},<ESC>
:将光标下移两行并在行尾插入},
。 -
q
:停止录制宏。qq
:清除q
寄存器并进入宏录制模式。@p
:执行寄存器p
中的宏。j
:将光标下移一行。q
:停止录制宏。
-
-
-
Macro to format a person and go to the next person(寄存器
q
)- 跳转到有
<person>
的行 qq@pjq
- 跳转到有
-
执行宏到文件尾
999@q
999
是一个大于文件行数的数字,确保宏将被执行足够多次。
-
手动移除最后的
,
然后加上[
和]
分隔符
-
-
Resources
vimtutor
是一个 Vim 安装时自带的教程,直接在shell里输入vimtutor
- Vim Adventures 是一个学习使用 Vim 的游戏
- Vim Tips Wiki
- Vim Advent Calendar 有很多 Vim 小技巧
- Vim Golf 是用 Vim 的用户界面作为程序语言的 code golf
- Vi/Vim Stack Exchange
- Vim Screencasts
- Practical Vim(书)
Exercises
1.完成 vimtutor
。备注:它在一个 80x24(80 列,24 行) 终端窗口看起来效果最好。
输入vimtutor
,跟着tutor了解vim的基本操作即可。
2.下载我们提供的 vimrc,然后把它保存到 ~/.vimrc
。 通读这个注释详细的文件 (用 Vim!), 然后观察 Vim 在这个新的设置下看起来和使用起来有哪些细微的区别。
daisy@Daisy:~$ sudo cp /tmp/vimrc /etc/vim
vimrc中的具体内容:
set nocompatible
: 这个配置指示Vim在启动时不使用Vi兼容模式,并启用一些方便的Vim功能。在.vimrc
文件存在时,Vim会自动进入非兼容模式,所以在这个文件中设置这个选项实际上是不必要的,但为了确保在其他方式加载该配置文件时(例如保存为foo
,然后用vim -u foo
启动Vim),这里还是包含了这个选项。
syntax on
: 这个配置启用语法高亮功能,使得在Vim中打开这个文件时能够进行语法高亮显示。
set shortmess+=I
: 这个配置禁用了默认的Vim启动信息,减少了启动时的冗长消息。
set number
: 这个配置显示行号。
set relativenumber
: 这个配置启用了相对行号模式。当同时启用number
和relativenumber
时,当前行显示真实的行号,而其他行(上方和下方)显示相对于当前行的行号。这在快速查看需要跳转到特定行的行号计数时很有用。
set laststatus=2
: 这个配置始终在底部显示状态栏,即使只有一个窗口打开。
set backspace=indent,eol,start
: 这个配置使得退格键的行为更加合理。默认情况下,无法在使用’i’设置的插入点之前进行退格。这个配置允许你在任何位置进行退格。
set hidden
: 默认情况下,Vim不允许隐藏具有未保存更改的缓冲区(即在任何窗口中都不显示的缓冲区)。这是为了防止你忘记未保存的更改,然后通过:qa!
退出。然而,这个配置允许隐藏缓冲区,因为有时候隐藏缓冲区是很有用的。
set ignorecase
和 set smartcase
: 这些配置使得搜索在目标字符串中的所有字符都是小写时是大小写不敏感的,但如果搜索字符串中包含任何大写字母,则搜索会变为大小写敏感。这使得搜索更加方便灵活。
set incsearch
: 这个配置启用了实时搜索,即在你输入搜索模式时就开始搜索,而不是等到按下回车键才开始搜索。
nmap
和 inoremap
命令:这些命令重新映射了一些默认的按键绑定。这里的映射将箭头键映射到显示提示消息,提示使用更合适的移动命令。这是为了防止使用不太高效的移动方式,例如按住h/j/k/l键进行移动而不是使用更有效的移动命令。
set noerrorbells visualbell t_vb=
: 这些配置禁用了听觉提示音,因为它们可能会让人感到烦恼。
set mouse+=a
: 这个配置启用了鼠标支持。虽然应该尽量避免过多依赖鼠标,但有时候它还是很方便的。
3.安装和配置一个插件: ctrlp.vim.
用 mkdir -p ~/.vim/pack/vendor/start
创建插件文件夹
下载这个插件: cd ~/.vim/pack/vendor/start; git clone https://github.com/ctrlpvim/ctrlp.vim
阅读这个插件的 文档。 尝试用 CtrlP 来在一个工程文件夹里定位一个文件,打开 Vim, 然后用 Vim 命令控制行开始 :CtrlP
.
自定义 CtrlP:添加 configuration 到你的 ~/.vimrc
来用按 Ctrl-P 打开 CtrlP
daisy@Daisy:/usr/share/vim/vim82/pack/vendor/start$ sudo git clone https://github.com/ctrlpvim/ctrlp.vim
针对CtrlP插件的设置:
let g:ctrlp_map = '<c-p>'
: 这个配置将CtrlP映射到Ctrl + P键。也就是说,当你按下Ctrl + P时,CtrlP插件将被触发并打开。
let g:ctrlp_cmd = 'CtrlP'
: 这个配置设置了CtrlP插件的默认命令为"CtrlP"。这意味着你可以在Vim中输入":CtrlP"来手动触发CtrlP插件。
let g:ctrlp_working_path_mode = 'ra'
: 设置默认路径为当前路径
daisy@Daisy:/etc/vim$ cat vimrc
"ctrlp plugin
set runtimepath^=~/usr/share/vim/vim82/pack/vendor/start/ctrlp.vim
let g:ctrlp_map = '<c-p>'
let g:ctrlp_cmd = 'CtrlP'
let g:ctrlp_working_path_mode = 'ra'
4.练习使用 Vim, 在你自己的机器上重做 演示。
这里是一个有bug的 fizz buzz 实现:
def fizz_buzz(limit):
for i in range(limit):
if i % 3 == 0:
print('fizz')
if i % 5 == 0:
print('fizz')
if i % 3 != 0 and i % 5 != 0:
print(i)
def main():
fizz_buzz(10)
我们会修复以下问题:
- 希望调用主函数
- 希望range从 1 开始
- 希望在 5 的整数倍的时候打印 “buzz”
- 希望在 15 的整数倍的时候在同一行打印 “fizz” 和 “buzz”
- 希望从命令控制行读取参数而不是采用硬编码的参数 10
修改后的代码为:
import sys
def fizz_buzz(limit):
for i in range(1, limit + 1):
if i % 3 == 0:
print('fizz', end='')
if i % 5 == 0:
print('buzz', end='')
if i % 3 != 0 and i % 5 != 0:
print(i, end='')
print()
def main():
fizz_buzz(int(sys.argv[1]))
if __name__== '__main__':
main()
具体修复bug的步骤:
-
希望调用主函数
-
G
文件尾 -
o
向下打开一个新行 -
输入
if name == '__main__':
main()
-
-
希望range从 1 开始
- 搜索
/range
ww
向后移动两个词i
插入文字, “1, “ea
在 limit 后插入, “+1”
- 搜索
-
希望在 5 的整数倍的时候打印 “buzz”
- 搜索
/fizz
n
,移动到下一个fizzci'
更改引号中的内容, 变为buzz
- 搜索
-
希望在 15 的整数倍的时候在同一行打印 “fizz” 和 “buzz”
gg
文件头,jjj
将光标下移,光标在print('fizz')
这行时,$
使光标移动到行尾,i
插入模式,敲入, end=’’
jj.
在print('buzz')
后面同样插入, end=’’
,.
可以重复之前的命令jjo
将光标下移并向下插入一行,敲入print()
,令每个数字的输出换行
-
希望从命令控制行读取参数而不是采用硬编码的参数 10
ggO
文件头,向上插入一行,输入import sys
/10
,定位到参数10处ci(
更改括号中的内容, 变为int(sys.argv[1])
daisy@Daisy:/tmp/missing$ python3 fizzbuzz.py 17
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
5.下个月用 Vim 完成所有的文件编辑。每当不够高效的时候,或者你感觉 “一定有一个更好的方式”时, 尝试求助搜索引擎,很有可能有一个更好的方式。
6.在其他工具中设置 Vim 快捷键 (见上面的操作指南)。
7.进一步自定义你的 ~/.vimrc
和安装更多插件。
8.(高阶)用 Vim 宏将 XML 转换到 JSON (例子文件)。 尝试着先完全自己做,但是在你卡住的时候可以查看上面宏 章节。