Vim 的哲学(四)

Vim 的哲学第四篇姗姗来迟,狗血的原因我就不多说了,好消息是我将为这个系列带来一些动态演示。原本我打算录视频的,但是文章都写了那么些篇了,现在再录视频似乎晚了些,所以我研究了一下如何录制高质量的 GIF 动画(第三方软件都不好用,最后我还是用 QuickTime 和一段脚本来完成录制,挺酷的~)。接下来先奉上第一弹:

查漏补缺

上一期的基础配置我遗漏了一个蛮重要的选项:shiftround,这个选项真的很贴心很好用,遗憾的是官方文档对此语焉不详。我特意 Google 了一下才发现挺少有人解释这个选项的。现在我也忘记了当初我是怎么知道它的,然后我发现用文字也还挺难解释清楚,所以还是看动画吧:

shiftround

round 在这里应该是取整的意思。当你的缩进不成倍时,开启这个选项将会让 Vim 自动帮你把周围的缩进化零为整,你就不需要手动去填/删空格了。顺便提前讲一下,缩进的指令是 <> 键,它们支持移动指令(马上讲到),也支持数字前缀。对当前行执行缩进是 <<>>,也就是连按两次。我们在以后会详细介绍关于缩进的知识。

OK,加上它在你的基础配置里,我们开始新的旅程!

移动与编辑

现在我们知道使用 Vim 的一个很重要的原则就是远离鼠标,远离可视化的定位装置(当然除了键盘以外),原因我就不再赘述了。在此原则之下,必然要有一些方式来帮助我们选中目标然后做出我们希望的更改,也就是移动和编辑。

在常规模式里有两个很重要的概念,一个叫做“动作”(Motions),另一个叫做“操作”(Operators)。动作,是指你能让光标移动到哪里,而操作则需要结合动作来决定你可以对文本做什么。通过二者的结合,我们可以逐渐体会 Vim 是如何贯彻保持简单这一原则的。

hjkl 虽然简单,但是在很多场合之下它们太没效率了,对么?从现在开始我希望你记住:我们通常不用 hjkl 做大量的光标移动。如果你发现你经常抽疯似的按这四个键在屏幕里来回移动,那你就已经错了!这四个键真正的主要用处是作为其他动作和操作的辅助键来使用的,随着我们的深入学习你会越来越理解这一点。

那么除此之外我们还有什么选择呢?

横向移动 :help left-right-motions

当我们以行/段为单位来审视我们的文本时,Vim 为我们提供了一些横向移动的快捷动作。

移动指令 移动效果
0 移动光标至行首
$ 移动光标至行尾
^ 移动光标至行首的第一个非空白符的字符
g_ 移动光标至行尾的最后一个非空白符的字符
f{char} F{char} 向前(右)或向后(左)移动光标至指定的字符({char})处,光标停留在该字符之上
t{char} T{char} 向前(右)或向后(左)移动光标至指定的字符({char})处,光标停留在该字符前/后面

通过上面这个表格,我们可以获取到以下信息:

  1. Vim 的许多指令都是成对儿的(非常重要),学习各种指令时尝试成对儿去练习会事半功倍。
  2. 空白符(空格,Tab)在 Vim 里是很重要的,许多命令都有不同的版本来应对有/没有空白符的状况。这主要是为了满足不同的人群习惯,比如说码字儿的不是很在乎多一两个空格,但是写代码的就不一样了。
  3. 有些命令脱胎于正则表达式(相信你注意到了),所以结合正则来学习也能帮助你理解它们。
  4. 最后两行的那几个移动指令可能有一点不好理解,但是它们实在是太有用了,请你亲自试一下。以后我再深入介绍它们。

这还没完,对于横向移动来说,最麻烦的是当“回绕”(wrap)出现的时候。所谓回绕是指,当一行(段)的字符数目超过屏幕的可视宽度范围时,编辑器会将超出的部分自动转移到下一行来显示,但是这并非换行,也就是说没有插入换行符,只是在显示上不让字符超出屏幕的最大宽度范围而已。

这个特性当然不是 Vim 特有的,几乎所有的编辑器都支持回绕,可是 Vim 对待回绕是和其他编辑器完全不同的,尤其是在编辑长文档时会让你觉得有些古怪。我不会在本章详细介绍关于回绕的一切,因为本系列面向的读者主要是程序员。对于编写代码这样的工作,只要你遵循良好的编码规范(你应该这么做),那就很少会出现应对回绕的情况。所以目前为止你学会上述四个动作指令就足够了。未来的某一天我会专门写一篇如何打造专业的 Markdown 编辑功能,在那时候我们再回过头来好好谈谈回绕。

现在我列出横向移动时如果遇到回绕行我们可以做什么,这样你可以自己试一下:

移动指令 移动效果
gh gj gk gl 让光标在回绕行内做四方向移动
g0 移动光标至当前回绕行的行首
g$ 移动光标至当前回绕行的行尾
g^ 移动光标至当前回绕行的行首的第一个非空白符的字符
gm 移动光标至当前回绕行的中间位置(或尽可能接近中间的位置)

关于 g

你或许已经注意到 g 键的多次出现了,似乎它和其他指令相互配合可以产生许多新指令。没错,在 Vim 中有那么几个“万能的”指令修饰键,g 是其中之一。如果你好奇还有多少指令是用 g 来修饰的,你可以键入 :help g 来查看一个列表。另外你应该知道这份列表其实是索引文档的一部分,你可以时常打开索引(:help index.text)来考察下自己对 Vim 到底有多熟悉。

纵向移动 :help up-down-motions

纵向移动的指令比较多,我还是先介绍几个简单并且最有用的:

移动指令 移动效果
gg 让光标跳转到文档的最开始处
G 让光标跳转到文档的最后一行
{count}G :{count} 让光标跳转到指定的行号,即 {count} 所代表的行号
{count}% 让光标跳转到指定的百分比位置,比如说第一行是 0%,最后一行是 100% 等
H 让光标移动到当前屏幕的顶部(High Position),不滚屏
M 让光标移动到当前屏幕的中部(Middle Position),不滚屏
L 让光标移动到当前屏幕的底部(Low Position),不滚屏

{count} 是一个前缀标记,意思是这个指令前面可以追加数字,比如说如果你键入 25G,那么光标就会移动到当前文档的第 25 行去。这个特性非常重要,Vim 的强大和灵活在很大程度上都仰仗类似的前缀特性。

G{count}G 其实是同一个指令,只是带上数字前缀与否会产生不同的效果,所以我分开写了。这是第一次,一旦你了解了这个特点,以后我就没必要分开了。

ggG 是一对经典的搭档,许多非常有用的操作都是它们俩配合完成的。举个例子,《Vim 的哲学》所有的文字都是在Vim 里完成的,每一次到最后我都要把它们复制粘贴到 SegmentFault 博客的发表页面做最后的检查并且发布,如何全部复制过去?有很多办法,我一般选择如下两种:

  1. 如果我使用 MacVim(GUI 环境),那么我先 command + a,然后 "+y 或者 "*y
  2. 如果我使用 CLI Vim(命令行环境),就变成一串指令:gg"+yG,拆开看:gg + "+y + G

喂喂,command + a 是全选吧,这是 Vim 的指令吗?你别忽悠我喔~

我可没有说你只能用 Vim 的内置指令呀,了解你所处的环境,在不影响效率,不打乱节奏的前提下善于利用一切可以利用的工具,这不正是极客的特质之一吗?只不过我不是每次都有机会用到 MacVim 的,所以 gg + G 的经典组合还是必须要掌握的。至于 "+y,这个涉及到寄存器(:help registers)的知识,我们稍后就会讲到,别急。

以词为单位的移动 :help word-motions

hjkl 是以字符为单位来移动的,横向和纵向移动基本上都是以句/段为单位来使用的,然而有些情况下我们需要的是介于二者之间的移动单位,也就是以词为单位。以词为单位使得我们可以更精确(也是更具语义化)的移动光标,并且要比逐个字符的移动要快得多。

这里所说的词特指的是像英语那样的以空格(有时候也会是别的符号)为分隔符的词——很遗憾,中文分词是很大的挑战,Vim 没有内置这一功能。像这样的词都可以分出词头和词尾,比如 word 这个词,词头是 w,词尾是 d

于是在 Vim 中的词组移动也按照词分头尾的特性分成了两组:

移动指令 移动效果
w b 向前(右)或向后(左)移动光标至下一个词组的词头位置
e ge 向前(右)或向后(左)移动光标至下一个词组的词尾位置

一些朋友喜欢寻找每一个指令对应的含义,这样有助于形象记忆。事实上,这些含义也不是我编造出来的,内置的文档里都有很详细的描述,比如说上面这几个分别是:

  1. w:Words forward
  2. b:words Backward
  3. e:forward to the End of word
  4. ge:backward to the End of word

你可能在想:我干嘛要关心词头和词尾,好麻烦啊!这个问题其实无关于 Vim 的哲学,而是语言的哲学。

你看,咱们的汉语和英语是完全不同的两个语种。在汉语里词与词的界限是靠意义来划分,书写形式则不太重要。用汉语写一句话,你可以在词组之间添加空格或者不添加空格,基本上不会对读者产生影响(除了个别会产生歧义的特例)。而英文及其他类似语言则不然,它们加或不加空格的差别大了去了!一段英文如果没有空格,那几乎就是无法阅读的。因此,对于母语为类似语种的人群来说,空格所划分出的词头与词尾是自然而然,司空见惯的事情,他们一点也不会觉得奇怪。不幸的是,作为程序员的我们也(被迫)得使用英语作为我们的主要书写语言,因此你必须习惯去辨识和使用词头与词尾。一旦你习惯了,你会发现它们非常有用。

为了让你看到正确使用词头词尾的效果,我放一张图给你对比一下差别。在这张图里,我分别演示了四种操作:

  1. 在句首,向右删除两个单词,使用词头作为动作指令(w
  2. 在句首,向右删除两个单词,使用词尾作为动作指令(e
  3. 在句尾,向左删除两个单词,使用词头作为动作指令(b
  4. 在句尾,向左删除两个单词,使用词尾作为动作指令(ge

每一步操作之后,注意观察光标停留的位置和最终的效果:

word_motions

这些结果或许是你期望的,也可能不是,但这不要紧,没有哪一种是绝对正确或错误的,重要的是你需要了解它们之间的差别,于是你可以在必要的时候选择正确的方式。

不过故事还没完,以上四个指令各自还有一个变体,分别是:WBEgE。要了解它们的作用,我们得先聊一下词的定界符。

对于词和词之间,空格是唯一的区隔标准吗?很显然不是。像这样的词:i_am_a_word,Vim 会视为 1 个词,但是 i-am-a-word,Vim 则会视为 7 个词!这是因为 Vim 允许你为其指定可以被视作词组定界符的字符,于是当 Vim 遇到这些字符的时候,就会认为是一个词的结束。默认情况下,_ 不是定界符,所以它会被视作一个词的组成部分。

然而有些时候我们希望把这些定界符也当作词组的一部分,这样我们可以移动的快速一点,这时大写版本的词组移动指令就派上用场了,它们永远都只把空白符(空格、TAB、EOL)视作词组的定界符。

你会觉得自己定义定界符很酷吧?我会把它放在高级设置那一篇来讲。

基础编辑 :help operator

Vim 内置了 15 个编辑指令(还有一些变体),但是一般来说我们用不到那么多。在本节我们来学习其中的五种(共计 10 个):

删除 :help d

如果你把光标对准某个字符,然后按下 d(delete),你会发现什么都没有发生?不要惊讶,编辑操作是要配合移动指令来干活的,我之前花大力气介绍一堆移动指令不是漫无目的的不是?

OK,精彩的来了。当你按下 d,Vim 会说:“好的伙计,你想要删除对吧?接下来请告诉我你要删什么?”

如果你要删一个词,按下 dw,也就是 delete word

如果你要删除两个词,按下 d2w 或者 2dw,它们的效果是一样的,但是它们代表的含义略有差别:

  • d2w 意思是:删除 -> 2 个 -> 词
  • 2dw 意思是:2 次 -> 删除 -> 1 个词

在这个例子里,两种操作的结果不会产生歧义,所以你能得到一样的效果。但是以后你会发现在某些特定的条件下,数字前缀在不一样的地方会产生不一样的效果(不只局限于删除操作)。所以,正确的理解操作的含义是有必要的,请记住:

理解操作的含义,而不是背诵操作的顺序。就好像你说话说的是你想要表达的意思,而不是字词的某种排列组合。有些时候你颠倒字词的顺序不会影响你要表达的意思,因为不存在歧义,但有些时候则正好相反,切记切记!

如果我要删除一整行怎么办?简单:dd

那如果我想要从光标的位置开始一直删除到行结束呢?那还用我教你?d$!不过 Vim 还有另外一个版本等价于 d$,它是:D

哦,那这么说如果我使用 0d$ 或者 0D,就是和 dd 等价的咯?

啊哈~聪明的童鞋,很抱歉你错了!但是不怪你,这是一个很重要的区别,我们来单独看一下示范:

dd_0D_0d$

看明白了吗?其实差异是非常明显的,dd 是连同行尾的行结束符(EOL)一起删除的,所以粘贴的时候也会连着行结束符一起粘贴;而 0D0d$ 则不会包含行结束符。

你可能还纳闷呢,不是演示删除的吗?为什么删掉的东西还能再粘贴回来呢?嗯,可能删除这个词不太恰当,如果改叫剪切是不是忽然就觉得贴切起来了?

Vim 的删除操作和我们常见的剪切非常相似,事实上 Vim 的删除指的是从你的眼前把目标文字移除到寄存器中,之后你还可以从寄存器里把删除的部分再粘贴回来。Vim 拥有一堆各式各样的寄存器,擅于使用寄存器可以让你的编辑工作变得异常轻松。今后我们会单独介绍寄存器的进阶使用。

让我来问你一个问题:如果要删除一个字符该怎么办?你或许已经了解到 x 可以删除光标所在的那个字符,X 可以删除光标左边的那个字符,但是你是否知道这两个功能也是从 d 演化出来的呢?试着找找答案吧。

改写 :help c

在你执行完删除之后,紧接着按下 i(insert),你就等于在改写之前删除的内容了。如何把这两步简化成一步?答案就是 c(change)了。对于 c,真的没什么好讲的,你学会了 d 就等于学会了 c,因为 c 就等于 d 完了紧接着 i 而已。

而且其他相关联的操作也是类似的,比如说:

  • c2w:改写两个词
  • cl:改写光标所在位置的字符,并且 s 等价于 cl
  • cc:改写光标所在的一整行,并且 S 等价于 cc
  • c^c$:从光标所在位置开始一直改写到行头/行尾,并且 C 等价于 c$

s 比较少用,因为通常改一个字符我们会使用 r,也就是替换(replace),但是 s 在编辑中文的时候有妙用,容我在这里卖个关子,等到打造专业 Markdown 编辑器的时候再说(实在是不能都说了,要不然这篇结束不了了)。

Scc 多节省一次按键,而且也比较好按(在标准键位上),所以推荐用 S 来代替 cc

复制 :help y

复制是几个基础编辑操作里怪癖略多的一个。首先是它的命名,yyank 的首字母,但是 yank 又是什么?它和拷贝(复制)有什么关系呢?

这也和寄存器有关。你看,Vim 的复制/剪切(删除)/粘贴操作都是基于它底层的寄存器的,由于 c 已经被改写(change)占用了,而 Vim 的复制实质上是把目标文本拉拽(yank)到寄存器中备用,所以……好了你知道了就是了,咱不解释那么多,反正 y 就是复制了,爱咋咋地~

另外一个怪癖出在 Y 身上,按照之前删除和改写的经验,你一定会认为 Y 就等同于 y$ 呗,(Vim 乱入:“呵呵,图样图森破!你以为我会让你这么轻易就掌握诀窍吗,少年?”)可是很不幸你又错了。这一回,Y 又和 yy 等价了……

我一直都没闹清楚为什么到了复制这里就和删除/改写不一样了,就连官方的帮助文档都是这么说的:

如果你希望 Y 是从光标处复制到行尾(这样更合乎逻辑,不过不兼容 Vi),你可以使用 :map Y y$

看起来唯一的原因就是为了和老 Vi 兼容,但是我们完全不在乎这一点!后面的 :map Y y$ 是键位映射,虽然我们还没讲到,不过这一句你已经可以把它放到你的 .vimrc 里了,重启 Vim 之后你会发现 Y 的表现和 C D 它们保持一致了,谢天谢地!

粘贴 :help p

粘贴就单纯多了,只有两个指令:

操作指令 移动效果
p 自光标所在位置向右粘贴默认寄存器里的内容
P 自光标所在位置向左粘贴默认寄存器里的内容

默认寄存器里的内容取决于你在粘贴前最后的编辑动作,有可能是删除或复制的一段文本,也有可能是其他的。由于我们还没有详细介绍强大的寄存器功能,你或许会偶尔感到有些不便,在这里我先介绍一个最常用的技巧:

有时候,我们需要完成如下操作:

  1. 在某处复制或删除(剪切)了一些文本
  2. 在另外一处删除一些文本
  3. 把之前复制或删除(剪切)的文本粘贴到这里

你看,这实际上是要用 A 处的文本来替换 B 处的文本,但由于默认寄存器只保留了最后一次的复制/删除(剪切)内容,所以当你完成第 2 步的时候,你在第 1 步准备好的文本已经没了……大多数人是这么做的:

  1. 在某处复制或删除(剪切)了一些文本
  2. 来到另外一处,先把这些文本粘贴到空白的地方
  3. 把需要替换的文本删除
  4. 清理多余的空白(如果需要的话)

实际上,我们可以让 Vim 不把指定的内容放入默认寄存器,这样就不会覆盖预先准备好的内容了,这等同于彻底删除而不是剪切。Vim 的默认寄存器是 ""(也叫匿名寄存器,:h quotequote),它保存常规的复制/删除等操作的内容,Vim 还有一个名字很酷的寄存器叫做:黑洞寄存器(Blackhole Register,:h quote_),它的按键是 "_。如果你在键入任何操作之前先输入 "_,操作的结果将不会被任何寄存器保留下来,就好像丢入了一个深渊黑洞,再也回不来了……(好伤感 T_T)

因此,我们可以这么玩:

  1. 在某处复制或删除(剪切)了一些文本
  2. 使用黑洞寄存器删除需要替换的内容,例如删除一行:"_dd
  3. 直接粘贴,搞定!

我把这个过程也录了下来,你可以对照看看:

blackhole_register

我真是爱死这玩意儿了!不过你要知道,就上例而言黑洞寄存器不是唯一的办法,说不定你更喜欢别的操作组合,比如下面这个:

without_blackhole

这一套“组合拳”没有用黑洞寄存器,它的好处是如果我反悔了,我还可以撤销之前的操作把被替换的内容找回来。整个过程的按键顺序是这样的:y$ -> gt -> gP -> D

请允许我用更加具有语义的方式来重复一遍上面的操作:

  1. y$:从光标所在位置(行首)复制到行尾(不包括换行符)
  2. gt:切换至下一个标签页
  3. gP:自光标位置向左粘贴刚才复制的内容,结束之后把光标向右移动一个字符(这就是 g 的作用,为了把末尾的 . 保留住。你也可以使用 Pl 实现一样的目标)
  4. D :自光标位置删除到行尾

我希望你理解我这样重复一遍的原因,它包含了体现 Vim 哲学的三个侧面:

  1. 每一步都保持简单的颗粒操作
  2. 从不死记硬背,而是去表达你的意图,用你自己的方式
  3. 条条大路通 Vim,何必死撞一棵树?

好吧,第三点纯粹是我在胡扯,哈哈。

大小写转换

大小写转换其实不算什么大事,本来我也犹豫还要不要介绍一下,但是考虑到这个在编程的时候还挺有用的,于是索性一并说了吧,反正也不多……

操作指令 移动效果
~ 转换光标所在字符的大小写(严格来说,这不是一个操作指令)
g~ 转换字符的大小写(这个才真的是)
gu 强制转换成小写
gU 强制转换成大写

解释一下头两个,~ 不是操作指令,是因为它没办法和移动指令结合,它就只会转换当前光标所在位置的那个字符。如果你有多个字符需要转换,你就只能一个一个按过去。g~ 才是转换大小写的正式版,它可以结合移动指令。比方说按下 g~3j 会把往下 3 行的字符大小写都转换了(小写变大写,大写变小写)。不过 Vim 有一个选项叫做 tildeop(:h tildeop),它默认是关闭的,如果你开启它,~ 就会变成和 g~ 一样了。这选项我记得很熟,因为我经常在团队里做重构工作,这种改写命名的活儿一再重复,我索性就把 ~ 变成真正的操作指令了。

另外,毫无意外的,它们几个都有直接操作当前一整行的快捷版本,分别是:g~~ guu gUU

趣味知识:你知道 ROT13 加密编码吗?这可能是世界上最简单的加密手段了,有意思的是 Vim 也内置了 ROT13 编码/解码功能。闲来无事的时候可以拿来逗别人玩哦!切换 ROT13 编码/解码的操作指令是:g?(:help g?)

缩进与排版

操作指令 移动效果
gq 自动应用排版规则
gw 自动应用排版规则(光标位置不变)
= 自动应用缩进规则
< > 手动应用缩进规则(左右两个方向)

前面两个在编写代码时不太常用,倒是在编写文档时能发挥作用,因此它们不是重点,请自行查阅文档并尝试。

后面两个就比较常用了,所谓“自动应用缩进规则”,前提是你得有可用的缩进规则。Vim 内置了非常多种语言的缩进规则,那些没有内置的也基本上都可以在网上找到合适的缩进规则插件。此前我们也在基础设置里打开了 filetype indent on,所以此时如果你打开一份源码文件,然后按下 gg=G,“唰”的一下——整个世界清静了。

还记得吧?gg 是去文件的最开始处,G 则是去文件的最后一行,所以这条命令的含义是:“从文件的开始处应用自动缩进规则直到文件的最后一行”。当然你可以不必总是对整个文件进行自动缩进,之前我们提到过的移动指令都可以搭配使用,之后我们还要介绍更加强大灵活的文本对象选择指令,搭配上自动缩进那叫一个如虎添翼~

至于 <> 就没什么新鲜的了,手动缩进呗!缩进的宽度是由 shiftwidth 指定的,咱们上次已经设置过了的有木有?另外它们也有针对当前行的快捷版本,你已经知道了,是吧?

想知道你的 Vim 内置了那些语言的缩进规则?键入这条命令::e $VIMRUNTIM/indent

Bonus

你现在可以尝试把这些最常用的指令应用在你的日常工作里了,如果你能坚持去寻找最有效率的操作方式,你终将会明白其实根本用不着装太多的插件。我很乐意进一步帮助你,所以你如果在使用中有任何疑问请不要客气尽管询问我,我也喜欢看看有什么新的挑战,所以来吧~


其实,第四篇本来想直接讲文本对象的,但是我担心新手会看不太懂,于是把文本对象一再往后挤。挤到现在才发现,天啊!这篇太长了,实在是不能再继续下去了。于是,我们只好对文本对象说拜拜了~咱们下期再见!


太极客(Very Geek)
As a designeer, I hope you can prove me wrong.

正在更新 Elixir 语言的系列文章:[链接]

31.1k 声望
3.1k 粉丝
0 条评论
推荐阅读
为 Koa 框架封装 webpack-dev-middleware 中间件
我见到有很多朋友在 SegmentFault 上面问一些不太好回答的问题,“JavaScript/Node 学好了能做什么?”,“前端架构师每天都做些什么?”等等。这些问题并非不能回答,但是第一、问题本身太过泛泛,很难回答的既针对...

n͛i͛g͛h͛t͛i͛r͛e͛25阅读 12.4k评论 6

千姿百态,瞬息万变,Win11系统NeoVim打造全能/全栈编辑器(前端/Golang/Ruby/ChatGpt)
我曾经多次向人推荐Vim,其热情程度有些类似现在卖保险的,有的时候,人们会因为一些弥足珍贵的美好暗暗渴望一个巨大的负面,比如因为想重温手动挡的快乐而渴望买下一辆二十万公里的老爷车,比如因为所谓完美的音...

刘悦的技术博客阅读 353

封面图
Vim入门
vim 用法vim +n 文件名 打开文件,光标会跳转到第 n 行;vim +/keyword 文件名 打开文件后,会高靓 keyword 关键字;==不生效了==vim 常用三种模式命令模式:该模式下不能对文件直接编辑,可以输入快捷键进行一些...

东城西决阅读 198

CodeGeeX、CodeWhisperer、Github Copilot三款AI辅助编程工具,程序员该如何选择?
亚马逊今天在Re:Mars大会上宣布推出CodeWhisperer,这是一款类似于CodeGeeX和GitHub Copilot的AI辅助编程工具,它根据一个注释或几个按键来自动补全整个函数。目前支持Java、JavaScript和Python,和CodeGeeX一样...

想发财的酱肘子阅读 139

正在更新 Elixir 语言的系列文章:[链接]

31.1k 声望
3.1k 粉丝
宣传栏