如何提高编程速度-Emacs高手教授轻松学习所有编辑器和IDE的秘诀

第1章 简介

  • 在一周到两周内学习主流的编辑器和IDE(Emacs, Vim, Visual Studio Code, IntelliJ IDEA, Sublime Text 3)的文本文件操作
  • 学习 Linux/Unix 的 Shell ,能够结合命令行程序和编辑器优化工作流
  • 学习用 Lisp 拓展神之编辑器 Emacs。 学习函数式编程技术
  • 第一部分对命令使用频率和快捷键设计的点评初学者节省精力和时间,为专家指出了优化效率的方向
  • 第二部分 Lisp 开发的例子是精心选择的.覆盖了程序员日常工作流.代码性能针对主流操作系统(Windows/Linux/macOS)都已优化

为什要提高速度

  • 预算和最后期限
  • 编程体验舒爽
  • 给自己发展多一点可能性

大纲

  • 快速编程技巧大全( 以 VScode 为例)
  • 学习编辑器之神 vim 的高效文本操作术
  • VScode 应用的文本文件操作术
  • 在其他编辑器和 IDE 应用些操作术
  • 在 emacs 中演示之前技巧的组合和拓展

科学数据提高编程效率

使用 keyfreq 统计数据,根据数据来优化我们的操作。

使用 vim 高效的文本操作术应用到 vscode 中。

单个文件中查找替换字符串的练习资料
test-code-emacs.zip 包含两个文件。 for-code.js 用来在vscode中练习单个文件文本操作。 for-emacs.js 的内容和 for-code.js 一样,是用来在emacs中练习文本操作的。在之后的视频中会多次用到。
取自于真实ReactJS项目的代码资料
用于练习 Emmet: Go to Matching Pair 命令, Go to Bracket 命令和 matchit 插件(reactjs-proj(1).zip)
多个文件多目录的文件文本操作练习资料
用于练习在多个文件中查找替换文本。也可用来练习多个子项目管理。在多个视频中有演示。(root-proj.zip)
Visual Studio Code 配置文件

此配置文件为了配合教程,做了一些简化。例如没有用VSCode的第三方插件matchit的快捷键覆盖VSCode默认的快捷键。

建议在学习前面几章时不要执着于快捷键。到学习"用Vim的技术强化Visual Studio Code”一章时在研究如何优化快捷键。

启用其中所有功能需要安装vim和matchit两个插件。

我的个人VSCode完整配置在 https://github.com/redguardtoo/vscode-setup 。在完成本教程前不建议使用此配置。

Sublime Text 3的配置文件
请将zip文件中的内容解压缩到`$MySt/Packages/User`目录下,$MySt指的是你的Sublime Text 3的配置目录(sublimetext3-setup.zip)
IntelliJ IDEA的插件IdeaVim的设置
ideavimrc.zip 解压缩后得到 .ideavimrc 将其放在环境变量$HOME所指向的目录里。这是IntelliJ IDEA的插件IdeaVim的配置文件。安装插件IdeaVim, 重启IntelliJ IDEA.
my-find-file和my-search-text实现
my-find-file.el 包含找文件和搜索文件两个功能的Emacs Lisp代码实现。建议照着对应课程完成所有代码后再看本文件对照。
company-mytags的实现
建议完成课程后再看代码
my-codenav的实现
建议完成课程后再看代码
my-syntax-check实现
建议完成课程后再看代码
my-spellcheck实现
建议完成课程后再看代码
my-evil-textobj实现
建议完成课程后再看代码

第2章 快速编程技巧大全

VSCode的精华只在于一个快捷键

文档:https://code.visualstudio.com/docs

  • 强调Execute All Commands需要大量使用,代替不常用的快捷键。此命令等价于Emacs的M-x命令。不要觉得这是人人都已掌握的常识。我可以断言,99%的用户 使用此命令的频率还是太低。不久之前我也是这99%的用户之一。- 只有在我用 “使用科学数据提高编程效率” (https://zhuanlan.zhihu.com/p/68182816) 一文中科学方法测量过之后,我才意识到很多我以为应该常用的命令实际上用得不多。不值得为记住其快捷键浪费脑细胞。所以我使用Emacs中的M-x次数才多了起来。

列出所有命令 Comand + Shift + p

Show All Commands: Ctrl + Shift + p  / mac Comand + Shift + p 列出所有命令。重要,关键字搜索方便查询命令
Open FIle:         Ctrl + o
Open Folder:       Ctrl + k Ctrl + o
Open Recent:       Ctrl + r
Keyboadrd Shortcuts Referentce        Ctrl + k Ctrl + r  帮助参考,打开一页面列出所有命令

mac 中 Command 键对应 Ctrl 键

下面我们根据 vscode 的菜单来学习一下。

学习VSCode中File菜单下所有功能

  • 很多资深用户也会在基本功上有遗漏。阻碍了他们的进一步提高。所谓基本功就是文本文件操作术。我的教程的目标就是补足所有的基本技术。所以会按照主菜单顺序讲解点评所有功能本教程讲述- 专家实战经验. “实战”这个词可能已被用烂了。网上大多数教程中“实战”两个字的意思就是照着手册“实际操作一遍”。要它们质疑大公司产品经理的决定是绝对不敢的。我肯定是在教程中要把所有不合理的决定都质疑一遍的 (陈斌:VSCode对Emacs,代码浏览哪家强)- 演示了如何充分利用Execute All Comands。少记好多快捷键。要把有限的精力放在少数几个常用的功能上。如伟人所教导的,”集中兵力打歼灭战“- 尽可能用电脑自动完成工作

可以通过 Show All Commands 来操作文件

  • 打开新文件: 输入 new untitiled file,快捷键 C-n
  • 打开新窗口: 输入 new window,快捷键 C-S-n
  • 打开文件:输入 open file, C-o
  • 打开文件夹: 输入open folder,当前作为工作区根目录 常用
  • 保存工作区: 输入save workspace,指定目录保存为一个文件,下次可以直接打开
  • 打开最近: 输入open recent
  • 一个项目中多个目录: 输入add foler to workspace,不常用
  • 自动保存:auto save,勾选上,不要使用手动保存功能
  • 关闭:close

vscode 中 Edit 菜单

Edit-Undo and Redo 功能

  • 撤消/恢复 undo/redo C-z/C-S-z 比较常用
  • 剪切/复制/粘贴 cut/copy/paste C-x/C-c/C-v 常用

Edit菜单下的查找替换

程序员最常用功能。在选定区域查找替换非常有用。

  • 当前文件中查找/替换 C-f/option-C-f 需要选中字符串
在选定区域内查找替(VSCode版和Emacs版)
  • 当前区域中查找/替换 C-f/option-C-f 需要选中字符串,弹出窗口中点 '三' 在选定区域内查找替换
    • 在 emcas 中快速选中区域可以使用 evil-matchit ,si 原生的为 c-@ 到 选中区域, 同时在缩小区域中 narrow-to-region
    • emcacs 中替换 evil '<,'>s/\<\(char1\)\>/char2/g ,退出缩小模式 widen C-x n w

Edit菜单下在多个文件中查找替换

在多个文件中查找替换是最有用的功能,大量用于重构和阅读代码。但是大多数程序员对此功能没有充分利用

在项目根目录文件中查找/替换

  • Find in Files: S-C-f
  • Replace in Files: S-C-h

可用选项:

  • 区分大小写
  • 全字匹配
  • 使用正则表达式
择包含与排除的目录

参考文档:https://code.visualstudio.com/docs/editor/codebasics#_search-across-files

点击 ... 可以选择包含与排除的目录。

golb 语法:

- * to match zero or more characters in a path segment
- ? to match on one character in a path segment
- ** to match any number of path segments, including none  匹配任意多个目录
- {} to group conditions (for example {**/*.html,**/*.txt} matches all HTML and text files)
- [] to declare a range of characters to match (example.[0-9] to match on example.0, example.1, …)
- [!...] to negate a range of characters to match (example.[!0-9] to match on example.a, example.b, but not example.0)

搜索放下方设置:拖拽搜索框到指定位置。

Edit 菜单下注释代码

注释代码是最常用最经典的场景,但是被大多数程序员忽视。实际上有很大的优化空间。

  • 行注释 Toggle Line Comment: ctrl + / mac Command + /, 后面使用 vim 来优化
  • 块注释 Toggle Block Comment: shift + alt + a ,不用记

Edit 菜单下 Emmet 工具箱扩展与缩写

Emmet那套自动完成html代码已被现代的web开发淘汰了。不过工具箱中其他工具还有可取之处。

官方地址:https://docs.emmet.io/

常用语法

input: div TAB
output: <div></div>

input: div*5
output:
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

input: div*test*5
<div id="test"></div>
<div id="test"></div>
<div id="test"></div>
<div id="test"></div>
<div id="test"></div>

比较古老的工具加快写html的速度,几乎所有编辑器都支持,要被淘汰,顶尖的程序员不会用这个,有了前端框架react等可用循环语句代替。https://stackoverflow.com/questions/38282997/rendering-an-array-map-in-react

this.state.data.map(function(item, i){
  console.log('test');
  return <li key={i}>Test</li>
})

标签开头结尾相互跳转:`emmet: Go to Matching Pair` 比较有用,没有快捷键。

vscode 中 Selection 选择

Selection下拓展/收缩选择区域和其他技巧

是程序员最常用的技巧,一定要记住其快捷键并尽量多用

  • 全选 Select All: C-a
    • 选中一段文本:shift 左/右
    • 以词为单位左右移动:ctrl 左/右
    • 选择区域以词单位移动:ctrl + shift 左/右
  • 扩大选择区 Expand Selection: Alt + Shift + 右,mac 上不同
  • 缩小选择区 Shrink Selection: Alt + Shift + 左,mac 上不同

Selection菜单下行操作

行拷贝很常用。

  • 下行操作
    • 向下复制一行 Copy Line Down:mac option + Shift + 下方向键,后面使用 vim 插件做重新设置,多模编辑技术

Selection菜单下Multi-Cursor技术

  • 切换为 “ctrl + 单击”进行多光标功能 Switch to Ctrl+Click for Multi-Cursor:,是比较古老的技术,现在比较常用 vi 的方式 C-v,按键会更少些。
  • 添加下一个词 Add Next Occurrence: C-d 。在用选中的词上 C-d 选中词,再 C-d 会匹配下个一样的字符,可以进行多编辑操作。
  • 选择所有匹配项 select all Occurences: S-C-L 选中所有匹配的词

比较古老的技术,现在比较常用 vi 的方式 C-v,按键会更少些。

vscode 中 View 查看

列出所有命令 Comand + Shift + p

View菜单下Open View

  • 打开视图 Open view

windows : 都支持按 Alt 不放开,根据菜单栏显示首字符快速选择

View菜单下Appearance菜单

有很多经典和常用的操作必须掌握,如zen-mode,如果你觉得没用。那说明你效率太低

  • Appearance 外观
    • 全屏 full screen:F11
    • 禅模式(Zen Mode): C-K Z 没有干扰的编辑 重要
    • 显示主侧栏(Menu Bar)、 边栏(Side Bar)、状态栏(Status Bar)、活动栏(Activity Bar)、面板(Panel)
    • 放大 zoom in: ctrl + = 常用
    • 缩小 zoom out: ctrl + - 常用
    • 恢复默认大小 reset zoom: C-0
    • 显示空格 Show Whitespace:调试时用

View菜单下Editor Layout菜单子窗口操作

编辑器布局是非常经典常用的操作。必须多加练习和应用

  • Editor Layout菜单(子窗口操作) 常用
    • Split Up 向上拆分窗口
    • Split Down 向下拆分
    • Split Left 向左拆分
    • Split Right 向右拆分
    • Single 唯一的窗口
    • 2x2 网络
    • Flip Layout 翻转布局

重要,后面结合 vi 来设置更有效的快捷键。用 Split Right,Split Down,Single 就可以了

View菜单下选择左边栏和下面板

不常用,不用记。

  • 资源管理器 explorer 文件浏览器。很少用,直接搜索就可以。
  • 搜索 search
  • 终端 terminal: C-`

View菜单下折叠行

  • 自动换行 toggle word wrap: 打开

vscode 中 Go 转到

*重要*,主要用来做代码导航,可以把命令对应的快捷键设置成自己习惯的。按常用性依次介绍。

Go菜单下跳转到另一个符号或当前符号的定义

程序员每时每刻必用功能,一定要记住并采用最优快捷键

  • 转到编辑器中的符号 Go to Symbol in Editor:常用,列出当前打开的文件里所有重要的函数、变量、类型定义等
  • 转到声明 Go to Definition:依赖语法的,如这个语言支持.
  • 转到类型定义 Go to Type Definition:常用,如果看到的是变量,这个变量的类型就是它的定义,会直接跳到变量类型定义
  • 转到实现 Go to Implemntation:如果定义了类,要把这个实现都显示出来

参考: https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition

Go菜单下以文件名搜索并打开项目中任意文件

  • 转到文件:go to file:C-p 打开当前项目的文件,可以匹配搜索。
  • 转到工作区中的符号 go o symbol in workspace:列出当前变量的定义

Go菜单下回退到之前编辑过的地方

非常经典的功能,一般和转到定义一起用

  • 返回 Back:C-t 常用,跳转到定义后返回到原来的位置。
  • 前进 Forward:ctrl + shift + -
  • 上次编辑位置 Last Edit Location:常用

Go菜单在子窗口和打开的文件间移动控制焦点

  • 切换编辑器 Switch Editor:切换到当前窗口在编辑的文件
  • 切换组 Switch Group:相当于子窗口切换操作, C-1 2 3 4
和跳转对应的Peek功能(看一眼但不动控制焦点)

输入命令 Peek Definition,会打开一个子窗口快速浏览,Ecs 退出。不常用。

Go菜单跳到指定行和在匹配括号间跳转

常用功能。匹配括号间跳转如结合Vim插件则是大杀器。后续课程会给出更多细节

  • 转到行/列 Go to Line/Column:ctrl + g 行号,快速跳到出错问题行号。
  • 转到括号 Go to Bracket:

Go菜单在跳到下一个语法错误

一般跳到第一个错误后就不需要再跳转了。自带的语法检查也好,第三方语法检查也好,都是实时检查错误。所以你不可能有两个以上错误。快捷键不用记。

  • 转到到下一个问题代码 Next Problem
  • 转到到上一个问题代码 Provious Problem

Go菜单下跳到下一个变化

  • 下一个更改 Next Change:常用,未放到版本控制服务器用绿色表式,会开一个小窗口。
  • 上一个更改 Previous Change

vscode 中 Debug菜单

不用记,可以使用更好的第三方工具

vscode 中 Terminal菜单

不建议用。 一般会使用独立的命令行窗口操作,代码比较清晰。

vscode 中 Help菜单

了解就行。

  • 列出所有命令 Keyboadrd Shortcuts Referentce: Ctrl + k Ctrl + r 帮助参考,打开一页面列出所有命令。
  • 报告问题 Report Issue:

如何选择高质量插件

  • 插件看评价,不看下载量

github 代码质量:

  • 看 issue,open 和 closed 比例:1:1 是差的,1:4 好, closed多表式关闭的bug多。closed 是用户关闭的,而不是作者关闭
  • pull requests 有人愿意修复这个bug
  • star 星数

第3章 学习Vim

安装Vim

编辑器之神Vim的文本操作效率公认非常高效。其他编辑器和IDE也支持Vim的快捷键。掌握了Vim就可以提高在所有编辑器和IDE中的效率

https://www.vim.org/

windows

  • PC: MS-DOS and MS-Window

https://ftp.nluug.nl/pub/vim/pc/gvim82.exe 双击运行,完全安装

官方教程:在开始菜单中找到 vim tutor

linux/mac

官方教程:在 shell 中输入 vimtutor 或中文 vimtutor zh

下面是教程每讲内容,在使用中学习,30分钟

Vim官方教程第一讲

移动光标

  • 移动光标
    • h: 左 L: 右 j: 下 k:
    • ecs 回到正常(noarmal)模式

VIM的进入和退出

  • VIM的进入和退出
    • 输入 <ESC> 确保处在正常模式,输入 :q! <回车> 放弃所有改动

文本编辑之删除

  • 文本编辑之删除
    • 在正常(Normal)模式下,可以按下 x 键来删除光标所在位置的字符

文本编辑之插入

  • 文本编辑之插入
    • 在正常模式下,按 i 在光标前插入文本
    • ecs 返回正常模式

文本编辑之添加

  • 文本编辑之添加
    • A 在一行后添加文本

编辑文件

  • 编辑文件
    • 使用 :wq 以保存文件并退出

小结

1. 光标在屏幕文本中的移动既可以用箭头键,也可以使用 hjkl 字母键。
         h (左移)       j (下行)       k (上行)     l (右移)
2. 欲进入 Vim 编辑器(从命令行提示符),请输入:vim 文件名 <回车>
3. 欲退出 Vim 编辑器,请输入 <ESC>   :q!   <回车> 放弃所有改动。
                      或者输入 <ESC>   :wq   <回车> 保存改动。
4. 在正常模式下删除光标所在位置的字符,请按: x
5. 欲插入或添加文本,请输入:
         i   输入欲插入文本   <ESC>             在光标前插入文本
         A   输入欲添加文本   <ESC>             在一行后添加文本
特别提示:按下 <ESC> 键会带您回到正常模式或者撤消一个不想输入或部分完整
的命令。

Vim官方教程第二讲

删除类命令

  • 删除类命令
    • 当前光标删除至下一个单词,请输入:dw

更多删除类命令

  • 更多删除类命令
    • 从当前光标删除至当前行末尾,请输入:d$

关于命令和对象,及操作

  • 关于命令和对象 许多改变文本的命令都由一个操作符和一个动作构成。
在正常模式下修改命令的格式是:
operator   [number]   motion

其中:
- operator - 操作符,代表要做的事情,比如 d 代表删除
- [number] - 可以附加的数字,代表动作重复的次数
- motion   - 动作,代表在所操作的文本上的移动,例如 w 代表单词(word),$ 代表行末等等。
一个简短的动作列表:
  w - 从当前光标当前位置直到下一个单词起始处,不包括它的第一个字符。
  e - 从当前光标当前位置直到单词末尾,包括最后一个字符。
  $ - 从当前光标当前位置直到当前行末。

正常模式下面仅按代表相应动作的键而不使用操作符,可以看到光标的移动正如上面的对象列表所代表的一样。

如:
de 会从当前光标位置删除到单词末尾

使用计数指定动作

在动作前输入数字会使它重复那么多次
  输入 2w 使光标向前移动两个单词。
  输入 3e 使光标向前移动到第三个单词的末尾。
  输入 0 (数字零) 移动光标到行首。

使用计数以删除更多

使用操作符时输入数字可以使它重复那么多次。
在正常模式下格式是:operator   [number]   motion

- 输入 d2w 以删除两个大写字母单词

操作整行

  • 操作整行
    • 输入 dd 可以删除整一个当前行。如 2dd 删除2行

撤消类命令

  • 撤消类命令
    • 撤消以前的操作,请输入:u (小写的u)
    • 撤消在一行中所做的改动,请输入:U (大写的U)
    • 多次输入 CTRL-R (先按下 CTRL 键不放开,接着按 R 键),这样就 可以重做被撤消的命令,也就是撤消掉撤消命令

小结

1. 欲从当前光标删除至下一个单词,请输入:dw
2. 欲从当前光标删除至当前行末尾,请输入:d$
3. 欲删除整行,请输入:dd

4. 欲重复一个动作,请在它前面加上一个数字:2w
5. 在正常模式下修改命令的格式是:
             operator   [number]   motion
   其中:
     operator - 操作符,代表要做的事情,比如 d 代表删除
     [number] - 可以附加的数字,代表动作重复的次数
     motion   - 动作,代表在所操作的文本上的移动,例如 w 代表单词(word),
                $ 代表行末等等。

6. 欲移动光标到行首,请按数字0键:0

7. 欲撤消以前的操作,请输入:u (小写的u)
   欲撤消在一行中所做的改动,请输入:U (大写的U)
   欲撤消以前的撤消命令,恢复以前的操作结果,请输入:CTRL-R

Vim官方教程第三讲

置入类命令

  • 置入类命令
    • 输入 p 将最后一次删除的内容置入光标之后

输入 dd 将该行删除,这样会将该行保存到 Vim 的一个寄存器中
输入 p 将该行粘贴置入

替换类命令

  • 替换类命令
    • 输入 r 和一个字符替换光标所在位置的字符

更改类命令

  • 更改类命令
    • 改变文本直到一个单词的末尾,请输入 ce,并进入插入模式

使用c更改更多

  • 使用c更改更多
    • 更改类操作符可以与删除中使用的同样的动作配合使用。格式为 c [number] motion
  • c$ 删除当前光标位置到行尾

小结

1. 要重新置入已经删除的文本内容,请按小写字母 p 键。该操作可以将已删除
   的文本内容置于光标之后。如果最后一次删除的是一个整行,那么该行将置
   于当前光标所在行的下一行。

2. 要替换光标所在位置的字符,请输入小写的 r 和要替换掉原位置字符的新字
   符即可。

3. 更改类命令允许您改变从当前光标所在位置直到动作指示的位置中间的文本。
   比如输入 ce 可以替换当前光标到单词的末尾的内容;输入 c$ 可以替换当
   前光标到行末的内容。

4. 更改类命令的格式是:

       c   [number]   motion

Vim官方教程第四讲

定位及文件状态

  • 定位及文件状态-行跳
G 最后一行
1G, gg 第一行
4gg, :4  第4行

输入 CTRL-G 显示当前编辑文件中当前光标所在行位置以及文件状态信息

搜索类命令

  • 搜索类命令
    • 输入 / 加上一个字符串可以用以在当前文件中查找该字符串
    • 逆向查找字符串,请使用 ? 代替 / 进行
      • n/N 向下向上搜索
    • CTRL-O 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
    • CTRL-I 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

配对括号的查找

  • 配对括号的查找
    • 如果光标当前位置是括号(、)、[、]、{、},按 % 会将光标移动到配对的括号上

vim-matchit 插件可以在函数、括号之间跳转

替换命令

  • 替换命令

    • 输入 :s/old/new/g 可以替换 old 为 new
    输入   :#,#s/old/new/g   其中 #,# 代表的是替换操作的若干行中
                             首尾两行的行号。
    输入   :%s/old/new/g     则是替换整个文件中的每个匹配串。
    输入   :%s/old/new/gc    会找到整个文件中的每个匹配串,并且对每个匹配串提示是否进行替换。
    

小结

  1. CTRL-G 用于显示当前光标所在位置和文件状态信息。
     G 用于将光标跳转至文件最后一行。
     先敲入一个行号然后输入大写 G 则是将光标移动至该行号代表的行。
     gg 用于将光标跳转至文件第一行。

  2. 输入 / 然后紧随一个字符串是在当前所编辑的文档中正向查找该字符串。
     输入 ? 然后紧随一个字符串则是在当前所编辑的文档中反向查找该字符串。
     完成一次查找之后按 n 键是重复上一次的命令,可在同一方向上查
     找下一个匹配字符串所在;或者按大写 N 向相反方向查找下一匹配字符串所在。
     CTRL-O 带您跳转回较旧的位置,CTRL-I 则带您到较新的位置。

  3. 如果光标当前位置是括号(、)、[、]、{、},按 % 会将光标移动到配对的括号上。

  4. 在一行内替换头一个字符串 old 为新的字符串 new,请输入  :s/old/new
     在一行内替换所有的字符串 old 为新的字符串 new,请输入  :s/old/new/g
     在两行内替换所有的字符串 old 为新的字符串 new,请输入  :#,#s/old/new/g
     在文件内替换所有的字符串 old 为新的字符串 new,请输入  :%s/old/new/g
     进行全文替换时询问用户确认每个替换需添加 c 标志        :%s/old/new/gc

vim-matchit 插件可以在函数、括号之间跳转
CTRL-O 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
CTRL-I 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

Vim官方教程第五讲

在 VIM 内执行外部命令的方法

  • 在 VIM 内执行外部命令的方法
    • 输入 :! 然后紧接着输入一个外部命令可以执行该外部命令

关于保存文件的更多信息

  • 关于保存文件的更多信息
    • 将对文件的改动保存到文件中,请输入 :w FILENAME

一个具有选择性的保存命令

  • 一个具有选择性的保存命令
    • 要保存文件的部分内容,请输入 v motion :w FILENAME
      • v 选中区域,按: 字符, 看到屏幕底部会出现 :'<,'> ,再按 :'<,'>w TEST 保存到 TEST 文件

提示:按 v 键使 Vim 进入可视模式进行选取。您可以四处移动光标使选取区域变大或变小。接着您可以使用一个操作符对选中文本进行操作。例如,按 d 键会删除选中的文本内容。

提取和合并文件

  • 提取和合并文件
    • 要向当前文件中插入另外的文件的内容,请输入 :r FILENAME
    • 读取外部命令的输出,如 :r !dir 可以读取 dir 命令的输出并将其放置到当前文件的光标位置后面

小结

1. :!command 用于执行一个外部命令 command。

   请看一些实际例子:
       (MS-DOS)         (Unix)
        :!dir            :!ls            -  用于显示当前目录的内容。
        :!del FILENAME   :!rm FILENAME   -  用于删除名为 FILENAME 的文件。

2. :w FILENAME  可将当前 VIM 中正在编辑的文件保存到名为 FILENAME 的文
   件中。

3. v motion :w FILENAME 可将当前编辑文件中可视模式下选中的内容保存到文件
   FILENAME 中。

4. :r FILENAME 可提取磁盘文件 FILENAME 并将其插入到当前文件的光标位置
   后面。

5. :r !dir 可以读取 dir 命令的输出并将其放置到当前文件的光标位置后面。

Vim官方教程第六讲

打开类命令

  • 打开类命令
    • 输入 o 将在光标的下方打开新的一行并进入插入模式
    • 输入大写的 O 可以在光标上方打开新的一行

附加类命令

  • 附加类命令
    • 输入 a 将可在光标之后插入文本
    • 输入大写的 A 可以在光标所在行的行末之后插入文本。
    • a、i 和 A 都会带您进入插入模式,惟一的区别在于字符插入的位置

另外一个置换类命令的版本

  • 另外一个置换类命令的版本
    • 输入大写的 R 可连续替换多个字符。

替换模式与插入模式相似,不过每个输入的字符都会删除一个已有的字符

复制粘贴文本

  • 复制粘贴文本
    • 使用操作符 y 复制文本,使用 p 粘贴文本

您还可以把 y 当作操作符来使用;例如 yw 可以用来复制一个单词

设置类命令的选项

  • 设置类命令的选项

    • 设置可使查找或者替换可忽略大小写的选项
      • Ignore Case,忽略大小写 `:set ic`
      • 如果您想要仅在一次查找时忽略字母大小写,您可以使用 \c ,如 /ignore\c <回车>
    :set xxx 可以设置 xxx 选项。一些有用的选项如下:
    - 'ic' 'ignorecase'       查找时忽略字母大小写
    - 'is' 'incsearch'        查找短语时显示部分匹配
    - 'hls' 'hlsearch'        高亮显示所有的匹配短语
    
    选项名可以用完整版本,也可以用缩略版本。
    
    在选项前加上 no 可以关闭选项:  :set noic
    

小结

1. 输入小写的 o 可以在光标下方打开新的一行并进入插入模式。
   输入大写的 O 可以在光标上方打开新的一行。

2. 输入小写的 a 可以在光标所在位置之后插入文本。
   输入大写的 A 可以在光标所在行的行末之后插入文本。

3. e 命令可以使光标移动到单词末尾。

4. 操作符 y 复制文本,p 粘贴先前复制的文本。

5. 输入大写的 R 将进入替换模式,直至按 <ESC> 键回到正常模式。

6. 输入 :set xxx 可以设置 xxx 选项。一些有用的选项如下:
      'ic' 'ignorecase'       查找时忽略字母大小写
      'is' 'incsearch'        查找短语时显示部分匹配
      'hls' 'hlsearch'        高亮显示所有的匹配短语
     
   选项名可以用完整版本,也可以用缩略版本。

7. 在选项前加上 no 可以关闭选项,如  `:set noic` 不忽略大小写, ':set nohlsearch'  移除高亮
8. 仅在一次查找时忽略字母大小写,您可以使用 `\c

Vim官方教程第七讲

获取帮助信息

  • 获取帮助信息
要启动该帮助系统,请选择如下三种方法之一:
    - 按下 <HELP> 键 (如果键盘上有的话)
    - 按下 <F1> 键 (如果键盘上有的话)
    - 输入	:help <回车>

  请阅读帮助窗口中的文字以了解帮助是如何工作的。
  输入 CTRL-W CTRL-W   可以使您在窗口之间跳转。
  输入 :q <回车> 可以关闭帮助窗口。

例如:
:help
:help s * 查看s替换的用法
:help c_CTRL-D  快捷键绑定
:help user-manual 用户手册

创建启动脚本

  • 创建启动脚本
1. 开始编辑 vimrc 文件,具体命令取决于您所使用的操作系统:
      :edit ~/.vimrc		这是 Unix 系统所使用的命令
      :edit $VIM/_vimrc	这是 MS-Windows 系统所使用的命令

2. 接着读取 vimrc 示例文件的内容:
      :r $VIMRUNTIME/vimrc_example.vim

3. 保存文件,命令为:
      :write

下次您启动 Vim 时,编辑器就会有了语法高亮的功能。
您可以把您喜欢的各种设置添加到这个 vimrc 文件中。
要了解更多信息请输入 :help vimrc-intro

补全功能

  • 补全功能
    • 确保 Vim 不是在以兼容模式运行: =:set nocp`
    • 使用 CTRL-D 和 <TAB> 可以进行命令行补全

范例:使用 CTRL-D 和 <TAB> 可以进行命令行补全

1. 请确保 Vim 不是在以兼容模式运行: :set nocp
2. 查看一下当前目录下已经存在哪些文件,输入: :!ls   或者  :!dir
3. 现在输入一个目录的起始部分,例如输入: :e
4. 接着按 CTRL-D 键,Vim 会显示以 e 开始的命令的列表。
5. 然后按 <TAB> 键,Vim 会补全命令为 :edit 。
6. 现在添加一个空格,以及一个已有文件的文件名的起始部分,例如: :edit FIL
7. 接着按 <TAB> 键,Vim 会补全文件名(如果它是惟一匹配的)。

小结:

1. 输入 :help 或者按 <F1> 键或 <Help> 键可以打开帮助窗口。

2. 输入 :help cmd 可以找到关于 cmd 命令的帮助。

3. 输入 CTRL-W CTRL-W  可以使您在窗口之间跳转。

4. 输入 :q 以关闭帮助窗口

5. 您可以创建一个 vimrc 启动脚本文件用来保存您偏好的设置。

6. 当输入 : 命令时,按 CTRL-D 可以查看可能的补全结果。
   按 <TAB> 可以使用一个补全。

要精通的话,可以阅读 Vim 的用户手册,使用的命令是: :help user-manual

推荐书: Vim - Vi Improved - 作者:Steve Oualline,这是第一本完全讲解 Vim 的书籍。它对于初学者特别有用。其中包含有大量实例和图示。

第4章 用Vim的技术强化Visual Studio Code 体验

把Visual Studio Code变成Vim

visual studio code 痛点:

  • 扩大缩小选择区域快捷键不方便按
  • 常用快捷键被占用,ctrl + 按键被其它的功能占用。

使用 vim 的好处:

  • 输入模式可以输入文本,同时可以结合 ctrl 键结合。
  • 正常(nomarl)模式 不能输入文本,解放了单字母键使用,如 hjkl 可以上下左右移动、文本操作命令。

安装 vim 插件:

  • 使用 ctrl + shift + p 输入 install extensions 在扩展中输入 vim 安装,之后就是 vim 中使用了。

模式显示在左下角

设置全局快捷键

比较关键,vscode 默认的是比较难按的。

全局快捷键设置

不依赖 vim 。

ctrl + shift + p 输入 shortcuts=,找到 =Preferences: Open Keyboadrd Shortcuts(JSON) 在打开的keybindings.json文件中添加

修改完文件会立即生效的。

例如:f12打开关闭操控面板panel

// 将键绑定放在此文件中以覆盖默认值
[
    {
        "key": "F12",
        "command": "workbench.action.togglePanel",
    }
]
  • 找到command全称: ctrl + shift + p 输入 default 找到 Preferences: Open Default Keyboard Shortcuts
  • 看看command是干什么的:如 togglePanel, ctrl + shift + p 输入 toggle panel

代码导航相关全局快捷键优化

设置几个必须的和vim兼容的,主要在代码导航时方便些

// 将键绑定放在此文件中以覆盖默认值
[
    {
        "key": "F12",
        "command": "workbench.action.togglePanel",
    },
    {
        "key": "Ctrl+]",
        "command": "editor.action.goToTypeDefinition",
    },
    {
        "key": "Ctrl+T",
        "command": "workbench.action.navigateBack",
    },
 {
  "key": "Ctrl+F12",
  "command": "workbench.action.toggleMaximizedPanel"
}
]
  • F12 :打开关闭控制面板。搜索根目录中包含字符串的文件。
  • ctrl + ] :跳到类型定义。和vim保持一致,vim中也设置这样的快捷键
  • ctrl + T:跳回。和vim保持一致,vim中也设置这样的快捷键

vim 中 跳转到文件和变量声明

# :help tagsrch.txt
CTRL-]:跳到类型定义
CTRL-t:跳回

# :help pattern.txt
gd       # 跳转到定义 go to declaration

# :help motion.txt
CTRL-O 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
CTRL-I 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

Visual Studio Code的全部设置

完整配置参考:https://github.com/redguardtoo/vscode-setup

安装好 vim 插件后,优化 vim 的快捷键

打开首选项:打开设置(json)。 ctrl + shift + p 输入 settings, 选择 open json 设置 用户settings.json。 如果想知道配置文件路径,可以使用 Copy path of active File 来查看当前活动文件路径。

完整文件内容:

{
  "vim.easymotion": true,
  "vim.sneak": true,
  "vim.visualstar": true,  // 开启 * # 搜索
  "vim.ignorecase": false,
  "vim.useSystemClipboard": true,
  "vim.useCtrlKeys": true,
  "vim.hlsearch": true,
  "vim.insertModeKeyBindings": [
    {
      "before": ["k","j"], //  插入模式下 按 k j 代表 ecs 退出到普通模式
      "after": ["<Esc>"]
    }
  ],
  "vim.leader": ",",
  "vim.visualModeKeyBindingsNonRecursive": [
    {
      "before": ["v"],  // 扩大选中的区域
      "commands": ["editor.action.smartSelect.expand"]
    },
    {
      "before": ["%"],  // 优化%符号跳转功能,需安装插件 matchit
      "commands": ["extension.matchitJumpItems"]
    },
    {
      "before": ["<leader>","x", "x"],  // 扩大选中的区域
      "commands": ["editor.action.smartSelect.expand"]
    },
    {
      "before": ["<leader>","z", "z"], // 缩选中的区域
      "commands": ["editor.action.smartSelect.shrink"]
    },
    {
      "before": ["<leader>","c", "i"], // 注释行,快捷键命名来自于 vim 插件 nerd-commenter
      "commands": ["editor.action.commentLine"]
    },
    {
      "before": ["<leader", "a", "a"], // ctrl-c 复制
      "commands": ["editor.action.clipboardCopyAction"]
    },
    {
      "before": ["<leader>","q", "q"], // 项目中多个文件文本查找
      "commands": ["workbench.action.findInFiles"]
    },
    {
      "before": ["<leader>","s", "s"],  // 当前文件搜索
      "commands": ["actions.find"]
    }
  ],
  "vim.normalModeKeyBindingsNonRecursive": [
    {
      "before": ["<leader>","r", "v"],
      "commands": ["editor.action.rename"]
    },
    {
      "before": ["<leader>","q", "q"], // 项目中查找替换多个文件
      "commands": ["workbench.action.findInFiles"]
    },
    {
      "before": ["<leader>","f", "p"],  // 拷贝当前文件路径
      "commands": ["workbench.action.files.copyPathOfActiveFile"]
    },
    {
      "before": ["<leader>","f", "n"],
      "commands": ["copyRelativeFilePath"]
    },
    {
      "before": ["<leader>","t", "p"], // 打开下拉菜单
      "commands": ["workbench.action.togglePanel"]
    },
    {
      "before": ["<leader>","x", "m"], // 显示命令面板
      "commands": ["workbench.action.showCommands"]
    },
    {
      "before": ["<leader>","c", "i"], // 注释行,快捷键命名来自于 vim 插件 nerd-commenter
      "commands": ["editor.action.commentLine"]
    },
    {
      "before": ["<leader>","x", "x"], // 扩大选中的区域
      "commands": ["editor.action.smartSelect.expand"]
    },
    {
      "before": ["<leader>","z", "z"], // 缩选中的区域
      "commands": ["editor.action.smartSelect.shrink"]
    },
    {
      "before": ["<leader>","t", "a"], // 显示活动栏
      "commands": ["workbench.action.toggleActivityBarVisibility"]
    },
    {
      "before": ["<leader>","t", "b"], // 显示主侧边栏
      "commands": ["workbench.action.toggleSidebarVisibility"]
    },
    {
      "before": ["<leader>","x", "s"], // 保存文件
      "commands": ["workbench.action.files.save"]
    },
    {
      "before": ["<leader>","s", "s"], // 当前文件搜索
      "commands": ["actions.find"]
    },
    {
      "before": ["%"], // 优化%符号跳转功能,需安装插件 matchit
      "commands": ["extension.matchitJumpItems"]
    },
    {
      "before": ["<leader>","s", "i"],
      "commands": ["extension.matchitSelectItems"]
    },
    {
      "before": ["<leader>","d", "i"],
      "commands": ["extension.matchitDeleteItems"]
    },
    {
      "before": ["<leader>","x", "f"],  // 打开文件
      "commands": ["workbench.action.files.openFile"]
    },
    {
      "before": ["<leader>", "x", "k"],
      "commands": ["workbench.action.closeActiveEditor"]
    },
    {
      "before": ["<leader>","r", "r"],  // 最近访问文件
      "commands": ["workbench.action.openRecent"]
    },
    {
      "before": ["<leader>","k", "k"], // 打开项目中任意文件
      "commands": ["workbench.action.quickOpen"]
    },
    {
      "before": ["<leader>","i", "i"], // 转到编辑器中的符号
      "commands": ["workbench.action.gotoSymbol"]
    },
    {
      "before": ["<leader>","x", "1"], // 仅一个窗口
      "commands": ["workbench.action.editorLayoutSingle"]
    },
    {
      "before": ["<leader>","x", "3"], // 向右垂直分屏
      "commands": ["workbench.action.splitEditorRight"]
    },
    {
      "before": ["<leader>","x", "2"], // 向下水平分屏
      "commands": ["workbench.action.splitEditorDown"]
    },
    {
      "before": ["<leader>","x", "4"], // 分 2*2 窗口
      "commands": ["workbench.action.editorLayoutTwoByTwoGrid"]
    },
    {
      "before": ["<leader>","x", "0"], // 关闭分组
      "commands": ["workbench.action.closeGroup"]
    },
    {
      "before": ["<leader>","x", "z"], // 打开终端
      "commands": ["workbench.action.terminal.focus"]
    },
    {
      "before": ["<leader>","f", "f"], // 禅模式
      "commands": ["workbench.action.toggleZenMode"]
    },
    {
      "before": ["<leader>","w", "h"], // 移动到左窗口
      "after": ["<C-w>", "h"]
    },
    {
      "before": ["<leader>","w", "j"],
      "after": ["<C-w>", "j"]
    },
    {
      "before": ["<leader>","w", "k"],
      "after": ["<C-w>", "k"]
    },
    {
      "before": ["<leader>","w", "l"],
      "after": ["<C-w>", "l"]
    },
    {
      "before": ["<leader>","w", "q"],
      "after": [":wq"],
    }
  ],
  "vim.handleKeys":{
    "<C-a>": false,
  },
  "zenMode.centerLayout": false,
  "window.zoomLevel": 1,
  "editor.minimap.enabled": false,
  "search.exclude": {
    "**/.git": true,
    "**/*.bundle.js": true,
    "**/bin-packages": true,
    "**/frontend-dist": true,
    "**/npm-packages-offline-cache": true
  },
  "search.useGlobalIgnoreFiles": true,
  "search.location": "panel",  // 弃用,搜索下移,新版本手动拖拽方式
  "workbench.activityBar.visible": false,
  "files.autoSave": "afterDelay",
  "workbench.colorTheme": "Solarized Dark",
  "workbench.statusBar.visible": true,
  "editor.renderWhitespace": "none",
  "editor.renderControlCharacters": false,
  "window.titleBarStyle": "native",
  "editor.renderLineHighlight": "none",
  "extensions.ignoreRecommendations": true,
  "editor.occurrencesHighlight": false
}

说明

# 设置leader键为 , 逗号
 "vim.leader": ","

# 插入模式下 按 k j 代表 ecs 退出到普通模式
  "vim.insertModeKeyBindings": [
    {
      "before": ["k","j"],
      "after": ["<Esc>"]
    }
  ],

# 在视图模式中使用
  "vim.visualModeKeyBindingsNonRecursive": [
...
   ]
# 在普通模式中使用
 "vim.normalModeKeyBindingsNonRecursive": [
 ...
   ]

后面会讲解每个快捷键要完成的任务。

File菜单的优化

普通模式下

最近访问文件

=,rr-

  • 最近访问文件:对应file–>最近–>更多
"vim.normalModeKeyBindingsNonRecursive": [
  {
    "before": ["<leader>","r", "r"],
    "commands": ["workbench.action.openRecent"]
  },
 ]

打开文件

,xf

  • 打开文件:和 emacs 中 C-x C-f 类似
"vim.normalModeKeyBindingsNonRecursive": [
  {
    "before": ["<leader>","x", "f"],
    "commands": ["workbench.action.files.openFile"]
  },
 ]

保存文件

,xs

  • 保存文件:按键和emacs中 c-x c-s 相似,方便记忆
{
  "before": ["<leader>","x", "s"],
  "commands": ["workbench.action.files.save"]
},

Edit菜单下优化

Edit菜单下 Undo/Redo和剪贴板操作

使用 vim 中的操作方式:

undo/redo:

  • u 撤销最近的更改,相当于windows中ctrl+z
  • 大U 撤消光标落在这行后所有此行的更改
  • Ctrl - r 重做最后的“撤消”更改,相当于windows中crtl+y

剪贴:

  • x:vim 中剪贴,d 也是删除

复制yink:

  • 选中区域按 y 复制 ,yy 复制当前行
{
  "before": ["<leader>", "a", "a"],
  "commands": ["editor.action.clipboardCopyAction"]
},

粘贴:

  • 按 p

额外说明:

剪切操作: vs code vim 插件的剪贴和系统剪贴板合成一个了,原始 vim 是分开的。所以选中文件按 y(yink) 等价于把文本内容拷贝到剪切板了,按 p 相当于剪切板中 paste 操作。

Edit菜单下查找替换的优化,学习正则表达式

查找字符

,ss

或使用 vim 自带的 / 搜索

视图和普通模式下

{
  "before": ["<leader>","s", "s"],
  "commands": ["actions.find"]
},

推荐选中正则模式来搜索,如 w.*

vs 正则手册(30分钟) https://docs.microsoft.com/en-us/visualstudio/ide/using-regular-expressions-in-visual-studio?view=vs-2022

替换

vim 插件

world
:%s*(wor)(ld)*$1=$2*g
# 在原生的 vim 和 emacs 中
:%s*\(wor\)\(ld\)*\1=\2*g
  • 在 emcas 中快速选中区域可以使用 evil-matchit ,si 原生的为 c-@ 到 选中区域, 同时在缩小区域中 narrow-to-region
  • emcacs 中替换 evil '<,'>s/\<\(char1\)\>/char2/g ,退出缩小模式 widen C-x n w

Edit菜单下注释的优化,行选择的技巧

`,ci`

视图和普通模式下

快捷键来自于 vim 插件 nerd-commenter,但有些人认为 ,cc 更方便些。

{
  "before": ["<leader>","c", "i"],
  "commands": ["editor.action.commentLine"]
      },

结合 vim 注释3行: V3j + ,ci

结合 emacs 注释3行: C-u 4 + ,ci

Edit菜单下项目中查找替换多个文件的高级技巧

多文件文本查找

,qq

视图和普通模式下

{
  "before": ["<leader>","q", "q"],
  "commands": ["workbench.action.findInFiles"]
},
多文件文本替换

范例:在多文本替换中,将 p1 替换为 project1

,qq 搜索字符并选中正则模式
匹配:(p)([0-9])
替换为:$1roject$2  # $0  整个匹配的模式, 原生 vim 中为 &
点击replace all 替换所有。
F12  关闭下拉面板

Selection菜单下选择文本的高级技巧

  • 全选:C-a

扩大选中的区域

,xx

视图模式和普通模式

{
  "before": ["<leader>","x", "x"],
  "commands": ["editor.action.smartSelect.expand"]
}

缩小选区

,zz

视图模式

{
   "before": ["<leader>","z", "z"],
   "commands": ["editor.action.smartSelect.shrink"]
 },

emacs 支持 key map 更方便,如:

扩大选择区: ,xx  再按x扩大
缩小选区: z
恢复:0

复制行

vim 中 `yy p`

多光标操作

使用 vim 代替

[number]<operator>[text object or motion]
<数字> <操作符> <文本对象或移动命令>

vit 选中tag内的
vat 选中包含tag

vi{ 选中括号内的
va{ 选中括号内的,包含括号

# 安装 vim-surround 插件后
cs`" 替换`为“  * vs code 中生效
ysw"  单词加绰号。 vs code 中生效。

View菜单下子窗口高级技巧

命令面板

,xm

即 vs code 中 ctrl + shift + p

普通模式下,

{
  "before": ["<leader>","x", "m"],
  "commands": ["workbench.action.showCommands"]
},

禅模式

,ff

ctrl k z 重要

普通模式

{
  "before": ["<leader>","f", "f"],
  "commands": ["workbench.action.toggleZenMode"]
},

分屏

,x1 ,x2 ,x3 ,x4 ,x0

普通模式,1个屏,右分割,水平分割,开4个子窗口,关闭当前窗口 跟 emacs 类似

{
  "before": ["<leader>","x", "1"],
  "commands": ["workbench.action.editorLayoutSingle"]
},
{
  "before": ["<leader>","x", "3"],
  "commands": ["workbench.action.splitEditorRight"]
},
{
  "before": ["<leader>","x", "2"],
  "commands": ["workbench.action.splitEditorDown"]
},
{
  "before": ["<leader>","x", "4"],
  "commands": ["workbench.action.editorLayoutTwoByTwoGrid"]
},
{
  "before": ["<leader>","x", "0"],
  "commands": ["workbench.action.closeGroup"]
},

vim 中的分屏操作

Ctrl+w,s:水平分割,上下分屏。或者:sp
Ctrl+w,v:垂直分割,左右分屏,或者:vs

:close 关闭当前窗口
:only 关闭除当前窗口以外的所有窗口

Ctrl+w c   关闭分屏  关闭当前窗口
Ctrl+w,方向键 : 窗口间切换

<C-w>w 在窗口循环切换。如<C-w><C-w><C-w><C-w>
<C-w>h 切换到左边的窗口
<C-w>j 切换到下边的窗口
<C-w>k 切换到上边的窗口
<C-w>l 切换到右边的窗口
*移动
<C-w>L 左边窗口移动到右边
<C-w>H 右边窗口移动到左边

Go菜单下高级技巧

回退

Ctrl + t 和 vim emacs 保持兼容

ctrl + shift + p
输入 shortcuts,找到 Preferences: Open Keyboadrd Shortcuts(JSON)
在打开的keybindings.json文件中添加
    {
        "key": "Ctrl+T",
        "command": "workbench.action.navigateBack",
    }

转到类定义

Ctrl + ] 和 vim emacs 保持兼容

ctrl + shift + p
输入 shortcuts,找到 Preferences: Open Keyboadrd Shortcuts(JSON)
在打开的keybindings.json文件中添加
    {
        "key": "Ctrl+]",
        "command": "editor.action.goToTypeDefinition",
    },

上次搜索位置

使用 vim 的操作

CTRL-O 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
CTRL-I 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

搜索并打开项目中任意文件

,kk

普通模式

{
  "before": ["<leader>","k", "k"],
  "commands": ["workbench.action.quickOpen"]
},

定位及文件状态-行跳

使用 vim 操作

G 最后一行
1G, gg 第一行
4gg, :4  第4行

输入 CTRL-G 显示当前编辑文件中当前光标所在行位置以及文件状态信息

在配对的符号间跳转的高级技巧

转到括号 Go to Bracket,非常重要,使用 vim 中的操作:

  • 光标放在当前标记下 (、)、[、]、{、} ,按 % 查找配对的标记,再按可跳回配对标记

范例:使用场景,在选中区域中替换

# 原文
function minus(var1) {
    const delta = 5;
    const b;
    const r= var1 + delta
    return r;
}

# 选中包含{}的内容
光标在{,按 v% 选中

# ,ss 搜索 delta
* ctrl + h 替换为 alpha

但存在一些问题,如光标必须在指定位置,不支持 html 标记,目前可用 matchit 插件解决,作者chenbin。emacs 中作者也写了一样的插件 evil-matchit

matchit jump items 跳到标记结尾
matchit select items 选中整个函数开头结尾
matchit delete items

添加settings.json配置

{
  "before": ["%"],
  "commands": ["extension.matchitJumpItems"]
},

代码自动完成的高级技巧

完成单词

vs code 自带的,当输入命令有列表选项,ctrl + n 向下选,ctrl + p 向上选

vim 中补全 ctrl + n

完成行

vim 中 C-x C-l 整行补全。vs code 使用 vim 插件后同样快捷键

可选项

跳转到声明(Go to Declaration)的高级技巧(可选)

理论上可选的。但实际上高手都用此技巧。

使用 vim

gd       # 跳转到声明 函数或者变量 go to declaration
ctrl + o # 回到原来位置

gd 在 vs code vim 插件内容实现用的 editor.action.goToDeclaration https://github.com/VSCodeVim/Vim/pull/530/files

Go to Symbol in Editor: 转到编辑器中的符号

视图和普通模式

{
  "before": ["<leader>","i", "i"],
  "commands": ["workbench.action.gotoSymbol"]
},

打开当前光标下的文件的高级技巧(可选)

也是可选的。但是高手都爱用。

vim

gf  # go to file
ctrl + o * 回到原来位置

在当前文件搜索文本的高级技巧(可选)

也是可选的。但是高手都爱用。

vim

* : 标记当前光标单词,跳至下一个出现该单词的地方。关闭高亮用 :set nohls
# : 与*类似,但是从下往上搜索

在视图模式下使用 *, # 搜索

* vs code 按了 vim 插件支持,settings.json 配置"vim.visualstar": true
* vim, emacs 要安装 vim-visualstar 插件支持
注意vs code 第一次按 * 星号 或者 # 井号,再使用 n 查找 ,vim/emacs 不存在此问题。

把VSCode的Vim插件和Neovim结合(可选)

可选,如果你已精通Vim和VSCode,希望简化工作流,把查找替换语法统一到Vim的原生语法,可以用此技术。

在 vs code 中字符替换还是 javascript 语法,如果使用原生 vim/emacs 支持的语法就需要和 neovim 结合。

world
:%s*(wor)(ld)*$1=$2*g  * javascript语法

# 在原生的 vim 和 emacs 中
:%s*\(wor\)\(ld\)*\1=\2*g

打开首选项:打开设置(json)

ctrl + shift + p 输入 settings
用户settings.json 添加如下内容
"vim.neovimPath": "/usr/bin/nvim",
"vim.enableNeovim": true,

提前安装 neovim 工具。https://github.com/neovim/neovim/releases/

小结

# 设置leader键为 , 逗号
# 插入模式下 按 k j 代表 ecs 退出到普通模式

# 文件操作 仅普通模式
最近访问文件:,rr 
打开文件:,xf 和 emacs 中 `C-x C-f` 类似
保存文件:,xs 和emacs中`c-x c-s`相似

# 编辑操作
undo/redo:vim 操作 uU/C-r。 . 点号重复操作,:e! 文档还原到最原始状态
剪贴:vim 中 x。d 也是删除
复制:,aa 选中复制。仅视图模式。vim 中 yy 复制当前行;选中 "+y 从 vim 拷贝到系统剪切板,"+p 系统到 vim
粘贴:vim 中 p
查找字符:,ss 。vim 中 / 。视图和普通模式下
替换:: :%s*(wor)(ld)*$1=$2*g  ,原生 vim 为 :%s*\(wor\)\(ld\)*\1=\2*g
注释:,ci 。结合 vim 注释3行:`V3j` + `,ci`;结合 emacs 注释3行:`C-u 4` + `,ci` 。视图和普通模式下
多文件查找:,qq 。 视图和普通模式下.

# 选择操作
全选:C-a
扩大选择区:按 v 或者 ,xx 。emacs 中扩大选择区: ,xx  再按x扩大。缩小选区: z。恢复:-1 。 视图模式和普通模式
缩小选择区:,zz 仅视图模式。emacs  支持 key map 更方便,扩大选择区: ,xx  再按x扩大, 缩小选区: z, 恢复:0
复制行:vim 中 `yy  p`
多光标操作:使用 vim 代替
- 列编辑:ctrl + v 向下移动, 大写 I 输入
- text object文件对象:
vit 选中tag内的
vat 选中包含tag
vi{ 选中括号内的
va{ 选中括号内的,包含括号
# 安装 vim-surround 插件后
cs`" 替换`为“  * vs code 中生效
ysw"  单词加绰号。 vs code 中生效。

# 视图下子窗口操作
命令面板:,xm 普通模式下
禅模式:,ff 普通模式
,xm:leader键,命令面板,仅普通模式
,ff:leader键,禅模式,仅普通模式
分屏:
,x1:leader键,仅保留当前子窗口,仅普通模式
,x3:leader键,右竖分屏,仅普通模式
,x2:leader键,水平分割,仅普通模式
,x4:leader键,开4个屏,仅普通模式
,x0:leader键,关闭当前屏,仅普通模式
- vim 分屏:
Ctrl+w,s:水平分割,上下分屏。或者:sp
Ctrl+w,v:垂直分割,左右分屏,或者:vs

:close 关闭当前窗口
:only 关闭除当前窗口以外的所有窗口

Ctrl+w c   关闭分屏  关闭当前窗口
Ctrl+w,方向键 : 窗口间切换

<C-w>w 在窗口循环切换。如<C-w><C-w><C-w><C-w>
<C-w>h 切换到左边的窗口
<C-w>j 切换到下边的窗口
<C-w>k 切换到上边的窗口
<C-w>l 切换到右边的窗口
移动
<C-w>L 左边窗口移动到右边
<C-w>H 右边窗口移动到左边

# go
- 跳转操作
C-t:回退。和 vim emacs 保持兼容
C-]:转到类定义
,kk:搜索并打开项目中任意文件,仅普通模式

- 上次搜索位置(vim)
C-o 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
C-i 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

- 定位及文件状态(vim)
G 最后一行
1G, gg 第一行
4gg, :4  第4行
C-g:显示当前编辑文件中当前光标所在行位置以及文件状态信息

%:配对的符号间跳转,光标放在当前标记下(、)、[、]、{、}

- 代码自动完成
C-n:vim 中完成单词,ctrl + n 向下选,ctrl + p 向上选
C-x C-l:完成行

- 可选的
  - 跳转声明(可选)
    ,ii:leader 键,转到编辑器的符号。vim 中用 gd,C-o 回到原位置

  - 打开当前光标下的文件(可选)
    gf:vim 中打开当前光标下的文件,C-o 回到原位置

  - 在当前文件搜索文本(可选)
    `*/#`:* 向下标记当前光标单词,# 相反。关闭高亮用 :set nohls

-  其它
kj:在插入模式下退回到普通模式
,xk:leader键,关闭当前编辑
,sp:leader键,回报社会

第5章 在Sublime Text中应用文本文件操作术

安装NeoVintageous并应用文本文件操作术

更新: Sublime Text的未公开的API文档的网站可能已下线。可以用https://web.archive.org/web/20191004202302/https://docs.sublimetext.info/en/latest/reference/api.html 替代

之前的操作术完全可以在 sublime 中使用。这里需要安装 NeoVintageous 插件。

地址:https://github.com/NeoVintageous/NeoVintageous

# window
git clone https://github.com/NeoVintageous/NeoVintageous.git "%APPDATA%\Sublime Text 3/Packages/NeoVintageous"

放在 ~/.config/sublime-text-3/Packages/User 的文件

.neovintageousrc 配置文件

" Type :help nv for help.
" @see http://docs.sublimetext.info/en/latest/reference/api.html for all commands
let mapleader= ","

noremap <leader>xm :ShowOverlay overlay=command_palette<CR> " 命令面板
noremap <leader>ci :ToggleComment<CR>
noremap <leader>xs :SaveAll<CR>
noremap <leader>aa :Copy<CR><ESC>
noremap <leader>zz :Paste<CR>
noremap <leader>yy :PasteFromHistory<CR>  " 从多个剪贴板中粘贴
noremap <leader>qq :ShowPanel panel=find_in_files<CR> " 搜索
noremap <leader>ss :ShowOverlay overlay=goto text=*<CR> " 当前文件搜索
noremap <leader>kk :ShowOverlay overlay=goto show_files=true<CR>  " 找文件
noremap <leader>ii :ShowOverlay overlay=goto text=@<CR> " 当前文件找出符号
noremap <leader>x1 :SetLayout cols=1 rows=1<CR> " 只有一个子窗口
noremap <leader>x2 :sp<CR>
noremap <leader>x3 :vs<CR>

<CR> 代表回车

额外命令配置

%APPDATA%\Sublime Text 3/Packages/User/Default.sublime-commands 文件,可关闭 vim 操作

[
  { "caption": "NeoVintageous: Toggle", "command": "toggle_neo_vintageous" }
]

引用的py配置 toggle_vintageous.py

import sublime
import sublime_plugin

class ToggleNeoVintageous(sublime_plugin.WindowCommand):
    def run(self):
        setts = sublime.load_settings('Preferences.sublime-settings')
        ignored = setts.get('ignored_packages')

        if 'NeoVintageous' in ignored:
            ignored.remove('NeoVintageous')
        else:
            ignored.append('NeoVintageous')

        setts.set('ignored_packages', ignored)
        sublime.save_settings('Preferences.sublime-settings')

Default.sublime-keymap 文件

[
  {
    "keys": ["k", "j"],
    "command": "_enter_normal_mode",
    "args": {
      "mode": "mode_insert"
    },
    "context": [
      {
        "key": "vi_insert_mode_aware"
      }
    ]
  }
]

第6章 在任意的IDE中应用文本文件操作术

在IntelliJ IDEA中安装使用IdeaVim

IntelliJ IDEA是全世界流行的IDE。应用之前教授的技术,以证明我们学到的技能是通用的。可以使用一生,永不会贬值。无论以后你使用何种编辑器和IDE,你都可以使用本课程学到的技巧

之前的操作术完全可以在 IntelliJ IDEA 中使用。这里需要安装 IdeaVim 插件

插件地址:https://github.com/JetBrains/ideavim

IdeaVim插件安装配置:
(1) 在File->Setting -> Plugins -> Browse repositories中查找IdeaVim插件安装即可。
(2) 重启  IntelliJ IDEA

# 将配置文件拷贝到
~/.ideavimrc

IntelliJ IDEA中的文本文件操作术

~/.ideavimrc 文件内容

let mapleader = ","   " leader is comma
let localleader = "," " leader is comma

set tabstop=4       " number of visual spaces per TAB
set softtabstop=4   " number of spaces in tab when editing
set shiftwidth=4    " spaces in newline start
set expandtab       " tabs are spaces
set number              " show line numbers
set rnu                 " show relative line numbers
set showcmd             " show command in bottom bar
set cursorline          " highlight current line
set surround            " use surround shortcuts
set commentary "vim-commentary
filetype indent on      " load filetype-specific indent files
set wildmenu            " visual autocomplete for command menu
set showmatch           " highlight matching [{()}]
set timeoutlen=500      " timeout for key combinations

set so=5                " lines to cursor
set backspace=2         " make backspace work like most other apps
set incsearch           " search as characters are entered
set hlsearch            " highlight matches
set ignorecase          " do case insensitive matching
set smartcase           " do smart case matching
set hidden

set fillchars+=stl:\ ,stlnc:\
set laststatus=2
set clipboard=unnamedplus  "X clipboard as unnamed

"press kj to exit insert mode
imap kj <Esc> " 输入模式下按 kj 进入到 normal 模式
vmap kj <Esc> " 视图模式下按 kj 进入到 normal 模式

"@see https://youtrack.jetbrains.com/issue/VIM-510 on expand selected region. Press `Ctrl-W` and `Ctrl-Shift-W` to increase and decrease selected region

noremap ,xm :action SearchEverywhere<CR>      " 命令菜单
noremap ,ci :action CommentByLineComment<CR>  " 注释
noremap ,xs :action SaveAll<CR>              " 保存所有
noremap ,aa :action $Copy<CR>    " 复制
noremap ,zz :action $Paste<CR>   " 粘贴
noremap ,yy :action PasteMultiple<CR>  “ 从多个剪贴板中粘贴
noremap ,qq :action FindInPath<CR>     " 搜索
noremap ,ss :action Find<CR>           " 当前文件搜索
noremap ,fp :action CopyPaths<CR>       " 拷贝当前文件路径
noremap ,xk :action CloseEditor<CR>    " 关闭编辑
noremap ,rr :action RecentFiles<CR>  " 最近访问文件
noremap ,kk :action GotoFile<CR>      " 跳到文件
noremap ,ii :action GotoSymbol<CR>    " 转到编辑器的符号
noremap <C-]> :action GotoImplementation<CR>  " 转到类定义
noremap ,xz :action ActivateTerminalToolWindow<CR> " 打开终端

" ideavim don't support numberic character in hotkey in 0.55
" it's fixed in 0.55.1
noremap ,x1 <C-W>o  " 仅保留当前子窗口,将 C-w o 映射到 ,x1
noremap ,x2 :split<CR>
noremap ,x3 :vsplit<CR>
noremap ,x0 :q<CR>
" move window
noremap ,wh <C-W>h
noremap ,wl <C-W>l
noremap ,wj <C-W>j
noremap ,wk <C-W>k
noremap ,xx :action EditorSelectWord<CR> " 扩展区域,选中当前区域不断扩展

帮助小技巧:

使用 :actionlist [pattern] 可知道原生快捷键关键词,如 :actionlist <C-W>

升级IdeaVim到预览版

IntelliJ IDEA的子窗口操作和ideavi

需要安装ideavim 0.55.1版本,见之前的章节

第7章 在Emacs中应用文本文件操作术

结合 vim 的知识,所有的文本能用的高级技术已经在之前 vscode 和 vim 中讲述。

官方地址:https://www.gnu.org/software/emacs/index.html

安装并学习Emacs和Evil

Windows

设置环境变量

调用 2 个环境变量,HOME 和 PATH。HOME 找到 emacs 的用户目录,PATH 找到 emacs 和 cygwin 里的命令。请提前安装 cygwin 服务。

方法1:右击桌面 我的电脑 > 属性 > 高经系统设置 > 高级 > 环境变量 > 用户变量 > 编辑

# HOME
c:/Users/jasper
# Path
D:\Program Files\emacs-27.2-x86_64\bin
D:\cygwin64\bin

方法2:win10 powershell 执行

# HOME 环境变量
[Environment]::SetEnvironmentVariable("HOME", "c:/Users/jasper/", "User")
#  Path
右击桌面 我的电脑 ...

运行它后,重新启动 PowerShell 使用 `dir env:`查看是否生效

# 查看当前环境变量和PATH
Get-ChildItem env:
$env:path -split ";"

下载 emacs 程序

Download at http://ftp.gnu.org/pub/gnu/emacs/windows/

这是官方的 GNU Emacs,由自由软件基金会为 Windows 构建。

这里有 32 或者 64 位的包,可以用 win 键查看“关于”电脑信息选择对应位的包。

image-20220115004658051.png

这里下载 emacs-27.2-x86_64.zip 包,解压到 /

启动 Emacs in Terminal vs GUI

terminal 终端启动: emacs -nw

GUI 启动:到 bin 目录下点击 runemacs 启动。addpm.exe 可把应用加到菜单中

Windows FAQ参考:http://xahlee.info/emacs/emacs/emacs_mswin.html

使用 chenbin 配置

一年成为Emacs高手

chen bin https://github.com/redguardtoo/emacs.d

rm ~/.emacs
cd ~; git clone https://github.com/redguardtoo/emacs.d.git .emacs.d

# 随机主题,在~/.custom.el中添加
(my-random-favorite-color-theme)

终端方式启动

emacs -nw

# 交互式调试
M-x ielm 
文本高级操作快捷键

可以使用 vim 的操作。文本高级操作快捷键都有。 如:

# 设置leader键为 , 逗号
# 插入模式下 按 k j 代表 ecs 退出到普通模式

# 文件操作 仅普通模式
最近访问文件:,rr 
打开文件:,xf 和 emacs 中 `C-x C-f` 类似
保存文件:,xs 和emacs中`c-x c-s`相似

编辑操作
undo/redo:vim 操作 uU/C-r。 . 点号重复操作,:e! 文档还原到最原始状态
剪贴:vim 中 x。d 也是删除
复制:,aa 选中复制。仅视图模式。vim 中 yy 复制当前行;选中 "+y 从 vim 拷贝到系统剪切板,"+p 系统到 vim
粘贴:vim 中 p
查找字符:,ss 。vim 中 / 。视图和普通模式下
替换:: ,rb:leader键,查找替换。选中字符 :'<,'>s/\<(选中的字符\)\>/要替换成的字符/g
注释:,ci 。结合 vim 注释3行:`V3j` + `,ci`;结合 emacs 注释3行:`C-u 4` + `,ci` 。视图和普通模式下
多文件查找:,qq 。 视图和普通模式下.

# 选择操作
全选:C-a
扩大选择区:按 v 或者 ,xx 。emacs 中扩大选择区: ,xx  再按x扩大。缩小选区: z。恢复:-1 。 视图模式和普通模式
缩小选择区:,zz 仅视图模式。emacs  支持 key map 更方便,扩大选择区: ,xx  再按x扩大, 缩小选区: z, 恢复:0
复制行:vim 中 `yy  p`
多光标操作:使用 vim 代替
- 列编辑:ctrl + v 向下移动, 大写 I 输入
- text object文件对象:
vit 选中tag内的
vat 选中包含tag
vi{ 选中括号内的
va{ 选中括号内的,包含括号
# 安装 vim-surround 插件后
cs`" 替换`为“  * vs code 中生效
ysw"  单词加绰号。 vs code 中生效。

# 视图下子窗口操作
命令面板:,xm 普通模式下
禅模式:,ff 普通模式
,xm:leader键,命令面板,仅普通模式
,ff:leader键,禅模式,仅普通模式
分屏:
,x1:leader键,仅保留当前子窗口,仅普通模式
,x3:leader键,右竖分屏,仅普通模式
,x2:leader键,水平分割,仅普通模式
,x4:leader键,开4个屏,仅普通模式
,x0:leader键,关闭当前屏,仅普通模式
- vim 分屏:
Ctrl+w,s:水平分割,上下分屏。或者:sp
Ctrl+w,v:垂直分割,左右分屏,或者:vs

:close 关闭当前窗口
:only 关闭除当前窗口以外的所有窗口

Ctrl+w c   关闭分屏  关闭当前窗口
Ctrl+w,方向键 : 窗口间切换

<C-w>w 在窗口循环切换。如<C-w><C-w><C-w><C-w>
<C-w>h 切换到左边的窗口
<C-w>j 切换到下边的窗口
<C-w>k 切换到上边的窗口
<C-w>l 切换到右边的窗口
移动
<C-w>L 左边窗口移动到右边
<C-w>H 右边窗口移动到左边

,uu 回退窗口,同时也有C-x 4 u

# go
- 跳转操作
C-t:回退。和 vim emacs 保持兼容
C-]:转到类定义
,kk:搜索并打开项目中任意文件,仅普通模式

- 上次搜索位置(vim)
C-o 带您跳转回较旧的位置。可跳到上一个文件搜索的旧搜索位置,可多次操作
C-i 则带您到较新的位置。可跳到下一个文件搜索的新搜索位置,可多次操作

- 定位及文件状态(vim)
G 最后一行
1G, gg 第一行
4gg, :4  第4行
C-g:显示当前编辑文件中当前光标所在行位置以及文件状态信息

%:配对的符号间跳转,光标放在当前标记下(、)、[、]、{、}

- 代码自动完成
C-n:vim 中完成单词,ctrl + n 向下选,ctrl + p 向上选
C-x C-l:完成行

- 可选的
  - 跳转声明(可选)
    ,ii:leader 键,转到编辑器的符号。vim 中用 gd,C-o 回到原位置

  - 打开当前光标下的文件(可选)
    gf:vim 中打开当前光标下的文件,C-o 回到原位置

  - 在当前文件搜索文本(可选)
    `*/#`:* 向下标记当前光标单词,# 相反。关闭高亮用 :set nohls

-  其它
kj:在插入模式下退回到普通模式
,xk:leader键,关闭当前编辑
,sp:leader键,回报社会

#----
ga:显示字符对应的 ASSCII 码
,ne:elisp请求检查 lazyflymake-goto-next-error
,dd:当前目录中搜索文本

以服务端形式启动 emacs

参考官方 :https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html*Emacs-Server

https://github.com/daviwil/emacs-from-scratch/blob/d24357b488862223fecaebdad758b136b0ca96e7/show-notes/Emacs-Tips-08.org

# 3 种启动守护方式
emacs --daemon
emacs --daemon=work # 可多进程守护
emacs --fg-daemon # 前台

目标是 emacsclientw.exe 的绝对路径后跟参数 -c -n -a "" ,运行这个快捷方式就可以以daemon模式启动Emacs。各参数的解释:

  • -c 新建一个frame。这个参数也让 emacsclientw 不要求提供文件名。
  • -n 立即退出,对于Windows来说这样就可以。
  • -a "" 这个参数很有意思。 -a 的本意是在Emacs没有运行的时候提供一个后退的编辑器,而提供一个空字符串则以daemon模式启动Emacs,这就是我们需要的。注意必须要带空字符串
emacsclient -f work -c -n

关闭 emacs 守护进程

emacsclient -e "(kill-emacs)"

学习Emacs官方教程

根据之前的文本文件操作术,对官方教程进行了精简优化,有些可以用 vim 来代替

使用不加载配置启动 emacs -q

M-x help-with-tutorial-spec-language * 可选中文
M-x
help-with-tutorial (C-h t)

这里用英文帮助

按键

Emacs 键盘命令通常包含 CONTROL 键(有时候以 CTRL 或 CTL 来标示)和 META 键(有时候用 EDIT 或 ALT 来标示)。为了避免每次都要写出全名,我们 约定使用下述缩写:

C-<chr>  按住 ctrl 键,同时按字母键。
M-<chr>  按住 alt 键,mac 中是 Option 键,同时按字母键。

如:
C-x C-c:退出 emacs
C-g: 退出一个在运行的命令Emacs 键盘命令通常包含 CONTROL 键(有时候以 CTRL 或 CTL 来标示)和
META 键(有时候用 EDIT 或 ALT 来标示)。为了避免每次都要写出全名,我们
约定使用下述缩写:
C-x k:关闭当前 buffer

iterm2 设置meta:

  • 打开偏好设置(command+,)
  • 选择 Profiles - Keys - General
  • 将Option键设置为Esc+,注意不要设置成Meta

跳转

# 光标移动
C-v:向下翻屏
M-v:向上翻屏
C-l:光标移到屏当中。很少用

C-n:向下移动
C-p:向上移动
C-b:按字符向左移移
C-f:按字符向右移移

M-f:按单词向右移移
M-b:按单词向左移移

C-a:移动到行首
C-e:移动到行尾

M-a:句子首 (很少用)
M-e:句子尾 (很少用)

段落操作基本用 vim 的方式
M-{|} 段落-首|尾
M-<|> 缓冲区-首|尾  vim 中 gg|G

几乎所有 emacs 都支持数字参数
用法:C-u 数字(常用),或者M-数字(不常用)
如 C-u 8 C-f 向右移动 8 个字符
不常用,一般开发时会用这个做调整,其他时候用命令,了解 emacs 支持数字概念就行。

停止响应命令

C-g:停止响应命令

windos 子窗口操作

C-x 1:仅一个当前子窗口,其它关闭。

如: C-h k C-f 再按 C-x 1

输入和删除文本

# 输入:键盘输入
插入文字很简单,直接敲键盘就可以了。
大部分的 Emacs 命令都可以指定重复次数,这其中也包括输入字符的
命令。如: C-u 8 *

# 输入 <Return> 
<Return> 是一个特殊的键,根据周围文本的不同,Emacs 可能会在换行符之后插入一些空白字符,这样,
当你在新的一行开始打字时,文本会自动与前一行对齐。

# 删除:
<DEL>        删除光标向前的字符Delete the character just before the cursor
C-d          删除光标向后的字符Delete the next character after the cursor

M-<DEL>      删除光标前的词Kill the word immediately before the cursor
M-d              删除光标后的词Kill the next word after the cursor

C-k          删除光标后的行Kill from the cursor position to end of line
M-k          删除当前句子(很少用)Kill to the end of the current sentence
PS:我们只需要学习字符、词、行的操作就行

C-<SPC> C-w:删除选中区域。使用 C-<SPC> 标记点,再移动到一个位置,C-w 删除。,C-<SPC> 往
往被中文用户设定成输入法热键,可用 C-@ 代替

# "killing" and "deleting" 的区别
killing 会被 emacs 记录下来,可以恢复。重新记录的 killed 文本叫 yanking。
C-y:yanking 的快捷键是 C-y (重要要记住)
如:C-k 删除一行,C-y 重新记录恢复
M-y:上一次 yanking 操作拉回来,可重复操作。很少用,推荐使用插件完成类似操作

UNDO 撤销操作

# undo
C-/:undo撤销操作。还可使用 C-_ 或者 C-x u
如:C-k 再按 C-/
C-x z 重复之前的操作 重复多次可以只按zzzz

# redo 反撤销
C-g C-/ 或者 C-g C-_ 或者 C-g C-x u

文件操作

在状态栏会看到类似 " -:---  TUTORIAL" 文本表示当前打开文件名
C-x C-f:打开文件
C-x C-s:保存文件, C-x C-s TUTORIAL.cn <Return> 另存为文件名

BUFFERS 缓冲区

使用 C-x C-f 打开文件,再切换回来还会看到打开的内容,是因为 emacs 会把这些文本内容放到 “buffer”中,每打开一个文件创建一个新 buffer。
C-x C-b:列出 buffer。每个文本有一个 buffer 名字。按 q 退出。
C-x b:切换 buffer
C-x s:保存 buffer (不常用)
# buffer
*Messages*:运行命令的信息都会出现在这里

命令集扩展

Emacs 的命令就像天上的星星,数也数不清。把它们都对应到 CONTROL 和 META 组合键上显然是不可能的。Emacs 用扩展(eXtend)命令来解决这个问题,扩展 命令有两种风格:

C-x     字符扩展。  C-x 之后输入另一个字符或者组合键。如 C-x C-c 退出 emacs 和用 C-x b 切换缓冲区
M-x     命令名扩展。M-x 之后输入一个命令名。

挂起,以切换到其他的应用程序


C-x:结合 vim 操作基本不用需要这个

M-x:非常常用。输入字符按 TAB 键支持补全

C-z:常用。linux 中把不用 C-z 要换成 C-x C-z,输入 fg 恢复到前台。

linux 中的作业管理

# 让作业运行于后台
运行中的作业: Ctrl+z
尚未启动的作业: COMMAND \&
后台作业虽然被送往后台运行,但其依然与终端相关;退出终端,将关闭后台作业。如果希望送往后台后,剥离与终端的关系
-    nohup COMMAND \&>/dev/null \&
-    screen;COMMAND
-    tmux;COMMAN

# 查看当前终端所有作业
jobs :+/- 表示优先级;当调用时不加作业号会默认调用+的命令

# 作业控制
-    前台作业:通过终端启动,且启动后一直占据终端
-    后台作业:可通过终端启动,但启动后即转入后台运行(释放终端)  
fg [[%]JOB_NUM]:把指定的后台作业调回前台
bg [[%]JOB_NUM]:让送往后台的作业在后台继续运行
kill [%JOB_NUM]: 终止指定的作业 

自动保存

默认自带,会把要保存的文件名自动保存为#文件名#。这个文件会在正常存盘之后被 Emacs 删除。
如:“helle.c” 自动保存“#hello.c#”

假如不幸真的发生了,你大可以从容地打开原来的文件,使用  M-x recover-this-file 恢复之前保存的版本。

我把自动保存的版本都放在 ~/.backups 目录下。自动保存的文件几乎不会用到,emacs 非常稳定。

回显区(ECHO AREA)

如果 Emacs 发现你输入多字符命令的节奏很慢,它会在窗格的下方称为“回显区” 的地方给你提示。回显区位于屏幕的最下面一行。也叫 minibuffer。例如输出信息, M-x 操作等。

状态栏(MODE LINE)

位于回显区正上方的一行被称为“状态栏”,也叫模式栏。会说明很多重要信息

-:**-  TUTORIAL.cn       63% L749    (Fundamental)

-NN%-- 显示的是光标在全文中的位置, 在开头--Top--,未尾 --Bot--,全部 --All--
“L” 和其后的数字给出了光标所在行的行号
最开头的星号(*)表示你已经对文字做过改动。刚刚打开的文件肯定没有被改动过,所以状态栏上显示的不是星号而是短线(-)
状态栏上小括号里的内容告诉你当前正在使用的编辑模式。缺省的模式是 Fundamental,就是你现在正在使用的这个。它是一种“主模式”。

emacs 有很 marjor mode,每种语言对应一个 marjor mode。

每一个 marjor mode 都会对生命令做一个强化或改变。比如写注释,因为每种语言的注释风格是不一样的。可切入不语言的 mode

# 切到 text-mode
M-x text-mode
C-h m:查看当前 mode 文档

除 marjor mode 外还有 minor mode ,它没有和编程语言绑定,是为加强 marjor mode 的。

搜索

C-s:向下搜索。
C-r:反向搜索(很少用)

但主要还是用 vim 的搜索, C-s 会保留一些额外加强版的命令

多子窗口

C-x 2:水平分割窗口
C-x 3:垂直分割窗口
C-x o:切换子窗口

在一个新开窗格里打开文件:
C-x 4 C-f,紧跟着输入一个文件,输入 C-x o 回到上方的窗格,然后再用 C-x 1 关掉下方窗格。

更高效的是 vim 操作方式

多FRAMES

图形界面中会常用到,命令行界面用不到。

C-x 5 2:新建
C-x 5 0:删除当前

帮助

一定要掌握

C-h a:查找命令
C-h ?:可以看到 C-h 后面有哪些可用的帮助

describe-varible C-h v     :寻找变量的帮助信息。如光标停留在变量上,C-h v
describe-function C-h f    :寻找函数的帮助信息  如save-buffer功能,使用C-h f再输入save-buffer
describe-bindings C-h b    :键绑定
describe-symbol C-h o      :符号

# 通过键盘 输入一个键绑定
describe-key-briefly C-h c :简述命令
describe-key C-h k         :寻找快捷键的帮助信息 如查看C-x C-s,使用C-h k再输入C-x C-s

C-h i:官方文档

Emacs的自由开放使其有一些独特的功能

以Emacs子窗口布局的特有功能说明自由软件的意义。以VSCode作为对比。

workgroups2 插件

[workgroups2](https://github.com/pashinin/workgroups2) 插件

可以保存工作窗口布局,下次直接使用。

Use M-x wg-create-workgroup to save save window&buffer layout as a work group. SPC-s s
Use M-x wg-open-workgroup to open an existing work group.  SPC-l l
Use M-x wg-kill-workgroup to delete an existing work group.

布局文件位置: ~/.emacs_workgroups

范例:

# 创建布局p2,2个垂直分屏
C-3 创建垂直分屏
SPC-s s 以当前布局为新建布局,新布局命名为 p2

# 另一个窗口重新加载布局
SPC-l l 列出所有保存的布局,选择 p2 布局

PS: 初学 emacs 尽量用高手的配置,等熟悉后再定自己的配置或者开发自己的插件。

如何测量并优化工作流

keyfreq 统计命令使用 插件

https://github.com/dacap/keyfreq

M-x keyfreq-mode
keyfreq-show:查看命令使用情况

范例:优化查找替换,用 vim 中搜索替换,而不是 . 点号重复操作。

# 1. 搜索替换
,rb:leader键,查找替换。选中字符 :'<,'>s/\<(选中的字符\)\>/要替换成的字符/g
# 2. 要恢复的内容
使用 git diff 来确认恢复哪些内容

查自己键盘的磨损情况,查自己命令的使用情况是否和高手一致,找到优化点。

文本编辑的核武器

原文:Nuclear weapon multi-editing via Ivy and Ag

就是在多文件中查找替换的技巧

,qq:多文件查找字符。这里用的 grep,没用 ag
C-c C-o:进入 ivy-occur 中。如果想排除某文件可输入正则 !code.js
C-x C-q:变成可编辑的
dd:排除不需要处理的行(可选)
,rb:查找替换。即 :'<,'>s/\<(选中的字符\)\>/要替换成的字符/g  。  这里用的是自己写的代码。
C-c C-c:完成

# 删除行
在 dd 换成 C-c C-d 删除行,此为 wgrep 插件源代码有写

发挥自己的主动性,读源代码。

git 设置能考: http://blog.binchen.org/posts/my-git-set-up/

少有人知道的打开文件的技巧

除Emacs之外的其他工具(包括Vim)在打开文件上总是有这样那样的遗憾之处。 只是演示,自主修改。

find-file-in-project 插件 https://github.com/redguardtoo/find-file-in-project 作者 chenbin

",kk" 'find-file-in-project-by-selected 找文件
",jj" 'find-file-in-project-at-point  在文本中找文件
",tt" 'find-file-in-current-directory   找目录, C-u 1 ,tt 在上层找目录

操作子窗口最理想的技巧

此技巧被著名的emacs配置spacemacs作者采用,已被许多Emacs用户采用

leader 键来优化操作。

窗口编号:数字加下划线,分配最方便的快捷键,使用 leader 键最方便。

插件

winum  https://github.com/deb0ch/emacs-winum
M-1 M-2 窗口切换,窗口编号在左下角。但 alt 键在不同的键盘上有差异,按起来不方便 。

使用 leader 键
,1  ,2 来切换窗口

其他插件:ace-window ,窗口编号在左上角

第8章 打造自由智能的开发环境

使用脚本和开源自由的命令行工具强化开发环境

拓展自己的能力 。

elisp 作为拓展编辑器的语言,它的功能是是非常强的,几乎所有的功能都能实现。如:

  • 代码导航
  • 代码自动完成
  • 在项目中找文件 ,jj 其中: ,bb 上个文件, pwd 当前路径

理解环境变量

设置 2 个环境变量即可。macOS 和 linux 都有不用额外设置

  • HOME: 在 windows 才会设置的。如 D:\cygwin64\home\Administrator
  • PATH: 可执行程序存放的目录。

在Windows上安装Cygwin

官方:https://www.cygwin.com/

emacs.d 配置中的一些插件需要使用 linux 中的 find, grep, ctags, git, vim 等命令。

image-20220115001718631.png

在macOS上安装Homebrew

https://brew.sh/

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

在Linux上使用Apt安装第三方软件

https://distrowatch.com/ 可以看到当前比较流行的 linux 版本。其中 Ubuntu 和 Debian 都是用的 apt 软件包管理工具。

sudo apt install git # 安装 tags, git等

Emacs Lisp基础: 变量

Emacs Lisp 用来配置 Emacs 和写Emacs 插件。

emacs lisp 是拓展编辑器的强大功能。我们不需要在记快捷键命令上浪费太多时间,只需要记住最关键常用的快捷键

常用命令

  • eval-expression:,ee 查看表达式的结果
  • eval-buffer:,eb 把缓冲区所有代码全部运行一遍

emacs 语法

任何东西都是函数,包括加号减号。

如:

# 拼接 2 个字符串
(concat  "abc" "def")  ;; "abcdef"  拼接 2 个字符串
(函数名 参数 参数)

变量

# 给变量赋值
(setq a 3)  ;; 3 给变量赋值
(setq b "abc") ;; "abc"   选中内容 va(,M-x eval-expression 按 C-y 回车执行命令

调试

使用 *Messages* 来调试

范例:

# 窗口1中输入测试变量
(setq a 3)
(setq b "abc")
(setq c "eeee")

# 切换窗口2显示调试信息
C-x 3 (,x3):  垂直切分屏
M-2:切到2号屏。winum 插件快捷键,或者使用 leader 键 ,2 来切换窗口,或者用自带的 C-x o
C-x b:  找到 *Messages*
# 切换回窗口1 执行变量
M-x eval-buffer (,eb):把缓冲区所有代码全部运行一遍
M-x eval-expression (,ee) ,输入 c 查看变量的结果
M-x erase-buffer:清空当前缓冲区

Emacs Lisp基础: 函数

;; define function
(defun hello(v1)
  "Hello worl"
  (message "hello world")
  (+ v1 3)
  )

使用

  • M-x eval-buffer (,eb):运行当前缓冲区所有命令
  • M-x eval-expression (,ee) :查看表达式的结果,回输入 (hello 3) 给出为 3

注意: 函数最后一行结果就是返回值,不用写 return。Messages 显示函数执行过程和函数返回值

函数文档开头需大写,如"Hello worl"

内置的函数

  • message 函数,打印一段信息,支持格式化打印

范例:message 函数

;; defind function
(defun hello (v1 v2 v3)
  (message "v1=%s v2=%s v3=%s" v1 v2 v3)
  )

使用:
M-x eval-buffer (,eb):运行当前缓冲区所有命令
M-x eval-expression (,ee) :查看表达式的结果
(hello a b c)  给出为 "v1=3 v2=abc v3=eeee"

可交互函数

函数中添加 interactive 就可以在加载完缓冲区命令后直接使用 M-x 调用这个函数了

范例:

;; function => interactive command
(defun bye ()
  (interactive)
  (message "bye world")
  (insert "this is a new line\n"))

;;;;;
this is a new line

使用:
M-x eval-buffer (,eb):运行当前缓冲区所有命令
M-x bye
其中 insert 函数,在当前缓冲区光标位置插入字符串

Emacs Lisp基础: 运算符

实际上运算符就是函数。

+ - * /

范例

(defun op (v1 v2 v3)
  (interactive)
  (message "(+ v1 v2 v3)=%s" (+ v1 v2 v3))
  (- v1 v2 v3)
  (* v1 v2 v3)
  (/ v1 v2 v3)
  )

使用:
M-x eval-buffer (,eb):运行当前缓冲区所有命令
M-x eval-expression (,ee) :查看表达式的结果,(op 3 4 5)

其它函数

(concat v1 "dddd" "ee" "333")) ;; 字符串拼接
(substring v1 0 1) ;; 取字符串中下标 0 - 1 之间的字符
(string-trim v1) ;; 去掉首尾空格
(string-equal v1 v2) ;;  比较字符串是否相等,t 为 true,false 为 nil
(setq d nil)
(setq d t)
  • C-h f 来查看函数的帮助
  • C-h C-f (需要提前配置好)跳转到函数源代码,使用 C-o 回退到旧位置,或者 C-x k 删除 buffer 来回退。
  • C-h k 查看快捷键绑定的函数

Emacs Lisp基础: 数据结构

list 函数

list 函数,返回 list 列表,比较常用。像 array, hastable 不常用,且性能不好

;; list a => b => c => e => list-fend
(setq my-list (list "a" "b" 1 2)) ;; ("a" "b" 1 2)  list 函数参数可以是变量
;; 简写 list 函数
(setq my-list2 '("a" "b" 1))    ;; ("a" "b" 1) 直接返回 list, 但不支持变量赋值运算

;; list 函数参数可以是变量
(setq a 3)
(setq b "abc")
(setq c "eeee")

(setq my-list3 (list a b c))  ;; (3 "abc" "eeee")
(setq my-list4 (list (+ 1 2) 3)) ;; (3 3)

list 操作

  • car 函数,取 list 头一个元素
  • cdr 函数,取 list 除头一个元素外的元素,返回一个新 list
  • nth 函数,取 list 中第 n 个元素,0 表示第一个元素。
(setq my-list2 '("a" "b" 1)
(message "car=%s" (car my-list2))  ;; "car=a"
(message "cdr=%s" (cdr my-list2))  ;; "cdr=(b 1)"
(message "nth(0)=%s nth(1)=%s" (nth 0 my-list) (nth 1 my-list2)) ;; "nth(0)=a nth(1)=b"   正常模式使用 vab 可选中括号中内容进行复制。
  • listp 函数,判断 list 是否是 list
  • length 函数,list 长度
;; listp stringp numberp
(message "listp=%s" (listp my-list)) ;; listp=t
(length my-list)   ;; 4
  • cons 函数,类似 list ,但参数只有 2 个
;; a => b => list-end   consp
(setq my-cons (cons "abcde" "bbb")) ;;  等价于 (setq my-cons '("abcde" . "bbb"))
(message "car=%s cdr=%s type of cdr=%s" (car my-cons) (cdr my-cons) (stringp (cdr my-cons)))  ;; car=abcde cdr=bbb type of cdr=t
(message "type=%s" (consp my-cons)) ;; type=t

Emacs Lisp基础: 正则表达式

参考:https://www.gnu.org/software/emacs/manual/html_node/elisp/Regular-Expressions.html

vs 正则手册 https://docs.microsoft.com/en-us/visualstudio/ide/using-regular-expressions-in-visual-studio?view=vs-2022

emacs 中的正则语法与 vim 比较接近。

常用的正则表达式函数:

  • replace-regex

(replace-regexp-in-string 正则 替换 字符串): 给定一个字符串搜索某个模式,然后把它替换掉。经常用来过滤不需要的文本

(string-match 正则 字符串): 正则表达式是否匹配字符串

(match-sting num string):把匹配到的取出来

;; replace-regexp-in-string
;; mactch-string string-match

;;
;; replace-regexp-in-string
;; 给定一个字符串搜索某个模式,然后把它替换掉
(setq str "abc1339def")
(setq a (replace-regexp-in-string "[0-9]+" "" str)) ;; "abcdef"


;; string-match 正则表达式是否匹配字符串
;; match-string 把上个匹配返回的
;; 字符串的 2 个斜杠会解释为 \(\) 类似于 grep 中的 group 组定义
(when (string-match "[a-z]*\\([0-9]*\\)[a-z]*" str)
  (message (match-string 1 str)))

Emacs Lisp基础: 语句(statement

`if when unless cond while dolist and or let* mapcar` 所有运算符都是函数

if

;; if when unless cond while dolist and or let* mapcar
;; 所有运算符都是函数

;; if 表达式为真只能执行后面一条语句,否则执行后面其它语句(if COND THEN ELSE...)
(setq my-cond t)
(if my-cond (message "if: my-cond true is true")  ;; my-cond 为真时执行
  (message "bye")       ;; my-cond 为假时执行
  (message "bye bye")   ;; my-cond 为假时执行
  )
;; if 语句一般不建议用,可以用其它语句替换

当 if 为真时要执行多条语句使用 progn 包含

(if (and (buffer-file-name) (buffer-modified-p))
    (progn
      ;; 把当前 buffer 的名字压进 autosave-buffer-list 列表, 用于后面的保存提示
      (push (buffer-name) autosave-buffer-list)
      (if auto-save-slient
          (with-temp-message ""
            (basic-save-buffer))
        (basic-save-buffer))
      )))

when

;; when 后面表达式为真时执行,否则者不执行(when COND BODY...)
(setq my-cond t)
(when my-cond
  (message "when: my-cond true is true")
  (message "bye"))
(when (not my-cond) ;; 布尔表达式,如果不为真执行下面语句
  (message "when: my-cond true is nil")
  (message "bye"))

unless

;; unless 等价于 not。如果不符合条件执行后面语句
;; (not OBJECT)
;; (unless COND BODY...)
(setq my-cond nil)
(unless my-cond
  (message "unless: my-cond nil is true")
  (message "bye"))

cond

;; cond 可以理解为siwtch case 语句,满足一个条件结束执行,(cond CLAUSES...)
(setq my-cond1 nil)
(setq my-cond2 nil)
(setq my-cond3 nil)
(cond
 (my-cond1
  (message "1"))
 (my-cond2
  (message "2"))
 (my-cond3
  (message "3"))
 (t
  (message "default"))
 )

while

;; while 满足条件,不断执行结构体内容 (while TEST BODY...)
(setq i 0)
(while (< i 5)
  (message "i=%s" i)
  (setq i (+ 1 i)))

and or

;; and or 是对 bool 表达式进行运算
;; (and 参数...) 求值args,只要其中一个生成nil,则返回nil。否则返回最后一个参数值。
;; (or 参数...)  求值args,只要其中一个生成true,则返回true 内容。否则返回最后一个参数值。
(message "and rlt=%s" (and t  2 3)) ;; 3
(message "and rlt=%s" (and nil 3)) ;; nil
(message "or rlt=%s" (or nil 3)) ;;3
(message "or rlt=%s" (or t 3))  ;; t
(message "or rlt=%s" (or 4 t 3))  ;; 4
(message "rlt=%s" (or (and t 4) 5)) ;; 4  嵌套
(setq i 0)
(while (and t (< i 5))
  (message "i=%s" i)
  (setq i (+ 1 i)))

(setq i 0)
(while (or nil (< i 5))
  (message "i=%s" i)
  (setq i (+ 1 i)))

dolist

;; dolist 循环 list 中取出每个元素一个一个进行操作。(dolist (VAR LIST [RESULT]) BODY...)
(setq my-list '(1 2 "a" "b"))
(dolist (elem my-list)
  (message "elem=%s" elem))
;;;
elem=a
elem=b
elem=2
elem=1

let*

;; let* 定义局部变量 (let* VARLIST BODY...)
;; let 老的语法,不支持变量重复利用
(let* ((a 3))
  (message "a=%s" a))

(let* ((a 3)
       (b (+ a 3)))
  (message "a=%s b=%s" a b))

mapcar

;; mapcar 给后面每个 sequence ( 数组或者list )运行函数 function,再接收函数返回值组成新的list。  (mapcar FUNCTION SEQUENCE)
(setq my-list '(1 2 "a" "b"))
(defun say-hi (elem)
  (message "Hi, elem=%s" elem)
  (format "str=%s" elem)) ;; 将返回值变成字符串
(message "mapcar=%s" (mapcar 'say-hi my-list)) ;; mapcar=(str=1 str=2 str=a str=b)

;; mapcar 调用 identity 函数,dentity 函数输入什么原样返回
(message "mapcar=%s" (mapcar 'identity my-list))  ;;mapcar=(1 2 a b)

;; mapcar 调用 匿名函数。虽然麻烦点,但是代码比较清晰
(message "mapcar=%s" (mapcar
                      (lambda (a)
                        (format "lambda=%s" a))
                      my-list))  ;;mapcar=(lambda=1 lambda=2 lambda=a lambda=b)

Emacs Lisp基础: 和命令行交互

和 shell 交互

shell-command-to-string

(setq a (shell-command-to-string "echo hello world")) ;; 将命令行执行的结果传给 a
(message "a=%s" a) ;; a=hello world

所有基础语法讲完,对于其它的语法,在写代码中实践就可以了。

Emacs Lisp免费电子书精选

比如简介中关于 Narrowing & Widening 介绍,文本操作经常用到这个功能。

Shell和命令行免费电子书精选

要点和书

入门基础

开源电子书集合,有很多高质量书箱:https://tldp.org/

提高

用Emacs Lisp开发第一个命令 hello

使用 emacs lisp 写自己的插件。

SPACE-dd :pwd,显示当前目录。

查看 ~/.emacs.d/init.el 文件配置, (load "~/.custom.el" t nil) 表示打开 emacs 会自动加载 ~/.custom.el 文件中的个人代码。

(defun hello ()
  (interactive)
  (message "hello world"))

C-x C-c 退出 emacs 再重新打开 emacs,使用 M-x hello 调用函数。

开发my-find-file原型

创建文件:~/projs/my-find-file/my-find-file.el

step 1 调用专业领域的命令行工具 find

定义命令行变量 cmd

(defun my-find-file ()
  (interactive)
  (let* (cmd)
    (message "cmd=%s" cmd)
    ))

;; M-x eval-buffer, M-x my-find-file
;; cmd=nil

测试命令行是对的

查找文件使用专业领域的命令行工具 find。 man find 查看帮助手册

  • 字符串两边加括号:v 选中字符串,按 i 进入插入模式,按 (

测试命令行是对的:

(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . -name \"*.*\"") )
    (message "cmd=%s" cmd)
    ))

;; M-x eval-buffer, M-x my-find-file
;; cmd=find . -name "*.*"

命令行输出保存到变量中,并把每行输出放在数组中

  • (split-string STRING &optional SEPARATORS OMIT-NULLS TRIM) 切割字符串。本例以换行符切割。
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . -name \"*.*\"")
         (output (shell-command-to-string cmd))
         (lines (split-string output "[\n\r]+")))
    (message "cmd=%s" cmd)
    (message "output=%s" output)
    (message "lines=%s" lines)
    ))

;; M-x eval-buffer, M-x my-find-file
;; output
cmd=find . -name "*.*"
output=.
./nvim.appimage
./.DS_Store
./etcd.config.yml.swp
./portable-ruby-2.6.8.arm64_big_sur.bottle.tar.gz
./myssh.sh
./AWSCLIV2.pkg
./my-find-file.el
./down/.DS_Store

lines=(. ./nvim.appimage ./.DS_Store ./etcd.config.yml.swp ./portable-ruby-2.6.8.arm64_big_sur.bottle.tar.gz ./myssh.sh ./AWSCLIV2.pkg ./my-find-file.el ./down/.DS_Store )

交互选择

让用户选中文件

使用 ivy 插件功能,配置中已加载此插件。

(ivy-read "Select: " '("hello" "world"))

;; output  可选择,返回选择的结果
2    Select:
hello
world

选择需要的文件:

(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . -type f -name \"*.*\"")
          (output (shell-command-to-string cmd))
          (lines (split-string output "[\n\r]+")))
    (message "lines=%s" lines)
    (setq selected-line     (ivy-read "Find file: " lines))
    (message "selectline=%s" selected-line)
    ))

打开文件

  • (find-file FILENAME &optional WILDCARDS) 打开文件
    • 支持正则匹配
(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . -type f -name \"*.*\"")
          (output (shell-command-to-string cmd))
          (lines (split-string output "[\n\r]+")))
    (message "lines=%s" lines)
    (setq selected-line     (ivy-read "Find file: " lines))
    (message "selectline=%s" selected-line)
    (find-file selected-line)
    ))
;; M-x eval-buffer, M-x my-find-file

调试代码兼容性,减少 bug

  • (file-exists-p FILENAME) 当文件变量非空,且文件存在时打开文件
(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . -type f -name \"*.*\"")
          (output (shell-command-to-string cmd))
          (lines (split-string output "[\n\r]+")))
    (message "lines=%s" lines)
    (setq selected-line     (ivy-read "Find file: " lines))
    (message "selectline=%s" selected-line)
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))
    ))
;; M-x eval-buffer, M-x my-find-file

让my-find-file排除特定目录

使用专业领域工具排除目录

> find . \( -path "*/.git" -o -path "*/.svn" \) -a -prune -o -print -type f -name "*.*"
.
./nvim.appimage
./.DS_Store
./etcd.config.yml.swp
./portable-ruby-2.6.8.arm64_big_sur.bottle.tar.gz
./myssh.sh
./AWSCLIV2.pkg
./my-find-file.el
./down
./down/.DS_Store
# 换成可以解决 find . \( -path "*/.git" -o -path "*/.svn" \) -a -prune -o  -type f -name "*.*" -print

*去除find中的点 . 点 *

  • (cdr LIST) 去除第一个元素
(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -print -type f -name \"*.*\"")
          (output (shell-command-to-string cmd))
          (lines (cdr (split-string output "[\n\r]+"))))
    (setq selected-line     (ivy-read "Find file: " lines))
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))
    ))
;; M-x eval-buffer, M-x my-find-file
10   Find file:
./nvim.appimage
./.DS_Store
./etcd.config.yml.swp
./portable-ruby-2.6.8.arm64_big_sur.bottle.tar.gz
./myssh.sh
./AWSCLIV2.pkg
./a.txt
./my-find-file.el
./down

my-find-file在项目根目录搜索

使用真实大型项目 linux

> find . -name "*.*" |wc -l
   73312
> cloc ./
   79447 text files.
   78892 unique files.
   11210 files ignored.

github.com/AlDanial/cloc v 1.90  T=75.61 s (902.9 files/s, 447811.7 lines/s)
---------------------------------------------------------------------------------------
Language                             files          blank        comment           code
---------------------------------------------------------------------------------------
C                                    32314        3276392        2578741       16882894
C/C++ Header                         23460         703974        1338664        6929246
reStructuredText                      3300         160571          64369         438061
JSON                                   508              2              0         360081
YAML                                  3151          57690          14530         265588
Assembly                              1328          47811         101201         229944
Bourne Shell                           900          27555          18983         107930
make                                  2803          10866          11819          49930
SVG                                     74             90           1171          48177
Perl                                    66           7383           5033          36556
Python                                 157           6813           6354          33942
Rust                                    37            948           5454           5539
yacc                                     9            700            409           4914
PO File                                  6            948           1088           3733
lex                                      9            346            309           2111
C++                                     10            373            138           2022
Bourne Again Shell                      56            392            322           1603
awk                                     13            217            157           1323
Glade                                    1             58              0            603
CSV                                     10             73              0            597
NAnt script                              2            154              0            545
Cucumber                                 1             34             58            196
TeX                                      1              6             74            156
TNSDL                                    2             33              0            140
Windows Module Definition                2             15              0            110
CSS                                      3             38             49            104
m4                                       1             15              1             95
Clojure                                 31              0              0             73
XSLT                                     5             13             26             61
MATLAB                                   1             17             37             35
vim script                               1              3             12             27
Markdown                                 1              8              0             25
Ruby                                     1              4              0             25
INI                                      1              1              0              6
sed                                      1              2              5              5
TOML                                     1              1              9              2
---------------------------------------------------------------------------------------
SUM:                                 68267        4303546        4149013       25406399
---------------------------------------------------------------------------------------

找项目根目录,写死根目录

一般不会从当前目录下找文件,更多的是从项目的根目录找文件。

  • default-directory is a buffer-local variable defined in C source code. 每一个 buffer 都有这个变量,当前绑定的目录。

开启2个窗口,分别打开自己代码、打开 linux 代码文件

/tmp/my-find-file.el

(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -print -type f -name \"*.*\"")
          (default-directory "~/tmp/linux-master")
          (output (shell-command-to-string cmd))
          (lines (cdr (split-string output "[\n\r]+")))
          selected-line)
    (setq selected-line     (ivy-read (format "Find file in %s: " default-directory) lines))
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))
    ))
;; M-x eval-buffer, 在 linux-master 任意子目录执行:M-x my-find-file
;; output
84629 Find file in ~/tmp/linux-master:
./init
./init/do_mounts_rd.c
./init/do_mounts.c
./init/do_mounts_initrd.c

优化方向

  • find 查找多文件本身已自动提速,还需要再提
  • 自动寻找项目根目录:(locate-dominating-file FILE NAME),项目根目录是硬编码,需要自动查找项目根目录。emacs 自带
    • (locate-dominating-file default-directory ".git") 当前目录向上层查找,有.git的目录就是根目录
(require 'ivy) ; optional
(defun my-find-file ()
  (interactive)
  (let* ( (cmd "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -print -type f -name \"*.*\"")
          (default-directory (locate-dominating-file default-directory ".git"))
          (output (shell-command-to-string cmd))
          (lines (cdr (split-string output "[\n\r]+")))
          slected-line)
    (setq selected-line     (ivy-read (format "Find file in %s: " default-directory) lines))
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))
    ))

;; M-x eval-buffer, 在 linux-master 任意子目录执行:M-x my-find-file

33行代码实现文件查找魔法

代码利用,即可在根目录查找,也可在当前目录查找

  • 更改目录位置
(require 'ivy) ; optional
(defun my-find-file-internal (directory)
  "Find file in DIRECTORY"
  (let* ( (cmd "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -print -type f -name \"*.*\"")
          (default-directory directory)
          (output (shell-command-to-string cmd))
          (lines (cdr (split-string output "[\n\r]+")))
          selected-line)
    (setq selected-line     (ivy-read (format "Find file in %s: " default-directory)
                                      lines))
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))
    )
  )
(defun my-find-file-in-project ()
  "Find file project root directory"
  (interactive)
  (my-find-file-internal (locate-dominating-file default-directory ".git")))

(defun my-find-file ()
  "Find file in current directory"
  (interactive)
  (my-find-file-internal default-directory))

;; M-x eval-buffer
;; 在 linux-master 任意子目录执行:M-x my-find-file   :M-x my-find-file-in-project

扩展在当前目录搜索

  • 给函数加可选参数。本例添加层级参数,1为上层,2为上2层目录,依次类推。

函数可选参数结合交互式参数组合

  • (defun NAME ARGLIST &optional DOCSTRING DECL &rest BODY)
  • (interactive &optional ARG-DESCRIPTOR &rest MODES)
    • p – Prefix arg converted to number. Does not do I/O. 参数转为数字
    • P – Prefix arg in raw form. Does not do I/O.
(defun my-find-file (&optional level)
  "Find file in current directory"
  (interactive "P")
  (message "leve=%s" level)
  (my-find-file-internal default-directory))

;; M-x eval-expression (my-find-file 1)  ;; level=1
;; 或者 C-u 1 M-x my-find-file ;; level=1
;; 或者 C-u  空 M-x my-find-file  ;; level=nil

上层目录

  • 返回文件路径,如果是目录/,则去掉/ (directory-file-name DIRECTORY)
    • (directory-file-name "abc/.git/") ;;output "abc/.git"
  • 返回文件的父目录,(file-name-directory FILENAME)
    • (file-name-directory "abc/.git") ;;outpu "abc/"
(defun my-find-file (&optional level)
  "Find file in current directory"
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
   ;; (my-find-file-internal parent-directory)
    (message "parent-derectory=%s" parent-directory)
   ))
;; M-x eval-buffer
;; M-x my-find-file ;;output parent-derectory=/Users/7tq6lr/Documents/wd/pfg/
;; C-u 2 M-x my-find-file ;;output parent-derectory=/Users/7tq6lr/Documents/

添加快捷键

推荐以 vim 插件为基础的 leader 键。

emacs/lisp/init-evil.el 已经添加了快捷键,见 my-comma-leader-def

~/.custom.el 文件

(my-comma-leader-def
 "xf" 'my-find-file
 )

my-find-file.el

(require 'ivy) ; optional
(defun my-find-file-internal (directory)
  "Find file in DIRECTORY"
  (let* ( (cmd "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -print -type f -name \"*.*\"")
          (default-directory directory)
          (output (shell-command-to-string cmd))
          (lines (cdr (split-string output "[\n\r]+")))
          selected-line)
    (setq selected-line     (ivy-read (format "Find file in %s: " default-directory)
                                      lines))
    (when (and selected-line (file-exists-p selected-line))
      (find-file selected-line))))

(defun my-find-file-in-project ()
  "Find file project root directory"
  (interactive)
  (my-find-file-internal (locate-dominating-file default-directory ".git")))

(defun my-find-file (&optional level)
  "Find file in current directory"
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
    ;;2,xf
    (my-find-file-internal parent-directory)))
;; M-x eval-buffer
;; 1,xf
;; 2,xf

以实战经验优化my-find-file

find 命令查找所有文件,第一次会慢同时也占用内存,没有利用 find 过滤功能。

改进:

  • 过滤文件名不区分大小文件名
find . \( -path "*/.git" -o -path "*/.svn"  \) -prune -o -type f -iname "*test*" -print
  • 读取用户输入的文件名关键字 (read-string "aa: ")
(read-string PROMPT &optional INITIAL-INPUT HISTORY DEFAULT-VALUE INHERIT-INPUT-METHOD)
在 minibuffer 中读取用户输入并返回
  • 比字符串是否相等。字符串不能为空,keyword 不为空返回 true。 (not (string= keyword "")
(require 'ivy) ; optional
(defun my-find-file-internal (directory)
  "Find file in DIRECTORY"
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ( (cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
              (default-directory directory)
              (output (shell-command-to-string cmd))
              (lines (split-string output "[\n\r]+"))
              selected-line)
        (setq selected-line  (ivy-read (format "Find file in %s: " default-directory)
                                          lines))
        (when (and selected-line (file-exists-p selected-line))
          (find-file selected-line))))))

(defun my-find-file-in-project ()
  "Find file project root directory"
  (interactive)
  (my-find-file-internal (locate-dominating-file default-directory ".git")))

(defun my-find-file (&optional level)
  "Find file in current directory"
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
    ;;2,xf
    (my-find-file-internal parent-directory)))
;; M-x eval-buffer
;; 1,xf
;; 2,xf

重用代码快速实现功能完整的my-search-text

搜索文本,和找文件是类似的。

  • 让 my-find-file-internal 不仅能找文件,也能搜索文本中关键字
  • 结合 grep 命令搜索文本:grep –exclude-dir=".git" –exclude-dir=".svn" -rns test .
  • 缩小视图:,ww
(defun my-file-internal (directory &optional grep-p)
  "Find file in DIRECTORY.
If GREP-P is t, grep files.
If GREP-P is nil, find files."
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ((find-cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
             (grep-cmd (format "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." keyword))
             (default-directory directory)
             (output (shell-command-to-string (if grep-p grep-cmd find-cmd)))
             (lines (split-string output "[\n\r]+"))
             (hint (if grep-p "Grep file in %s: " "Find file in %s: "))
             selected-line)
        (setq selected-line  (ivy-read (format hint default-directory)
                                          lines))
        (message "selected-line=%s" selected-line)
        ;; (when (and selected-line (file-exists-p selected-line))
        ;;   (find-file selected-line))
        ))))
;; M-x eval-buffer
;; M-x eval-expression (my-file-internal default-directory t)
;; output
19   Grep file in /Users/7tq6lr/Documents/wd/pfg/:
./KPI-2022.org:22:5. 压测(load test, spike test, soak Testcc)
./KPI-2022.html:306:<li>压测(load test, spike test, soak Testcc)<br /></li>

跳转到文件指定行

取出文件名和行号

  • 正则过滤字符串 (string-match REGEXP STRING &optional START)
  • 取出匹配的字符 (match-string NUM &optional STRING)
  • 字符串转换成数字 (string-to-number STRING &optional BASE)
  • 光标行动到第一行 (goto-char (point-min 1))。(goto-char POSITION) 将点设置为POSTTION, (point-min) 返回当前缓冲区的起点
  • 从当前光标位置向下移动 N 行 (forward-line &optional N)
  • 数字减 1:(1- NUMBER)
(defun my-file-internal (directory &optional grep-p)
  "Find file in DIRECTORY.
If GREP-P is t, grep files.
If GREP-P is nil, find files."
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ((find-cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
             (grep-cmd (format "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." keyword))
             (default-directory directory)
             (output (shell-command-to-string (if grep-p grep-cmd find-cmd)))
             (lines (split-string output "[\n\r]+"))
             (hint (if grep-p "Grep file in %s: " "Find file in %s: "))
             selected-line
             selected-file
             linenum)
        (setq selected-line  (ivy-read (format hint default-directory)
                                          lines))
        (cond
          (grep-p
           (when (string-match "\\(^[^:]*\\):\\([0-9]*\\):" selected-line)
             (setq selected-file (match-string 1 selected-line))
             (setq linenum (match-string 2 selected-line))
             (message "selected-file=%s linenum=%s" selected-file linenum)
             )
           )
          (t
           (setq selected-file selected-line)))
        ;; (when (and selected-line (file-exists-p selected-file))
        ;;   (find-file selected-file))
        ))))
;; M-x eval-buffer
;; M-x eval-expression (my-file-internal default-directory t)
;; output
selected-file=./KPI-2022.org linenum=22
(defun my-file-internal (directory &optional grep-p)
  "Find file in DIRECTORY.
If GREP-P is t, grep files.
If GREP-P is nil, find files."
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ((find-cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
             (grep-cmd (format "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." keyword))
             (default-directory directory)
             (output (shell-command-to-string (if grep-p grep-cmd find-cmd)))
             (lines (split-string output "[\n\r]+"))
             (hint (if grep-p "Grep file in %s: " "Find file in %s: "))
             selected-line
             selected-file
             linenum)
        (setq selected-line  (ivy-read (format hint default-directory)
                                          lines))
        (cond
          (grep-p
           (when (string-match "\\(^[^:]*\\):\\([0-9]*\\):" selected-line)
             (setq selected-file (match-string 1 selected-line))
             (setq linenum (match-string 2 selected-line))))
          (t
           (setq selected-file selected-line)))
        (when (and selected-line (file-exists-p selected-file))
          (find-file selected-file)
          (when grep-p
          (goto-char (point-min))
          (forward-line (1- (string-to-number linenum)))))
        ))))

复用在当前目录找文件和在项目中找文件代码

只需要函数调用时加上可选参数即可。(my-file-internal parent-directory t)

(require 'ivy) ; optional
(defun my-file-internal (directory &optional grep-p)
  "Find file in DIRECTORY.
If GREP-P is t, grep files.
If GREP-P is nil, find files."
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ((find-cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
             (grep-cmd (format "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." keyword))
             (default-directory directory)
             (output (shell-command-to-string (if grep-p grep-cmd find-cmd)))
             (lines (split-string output "[\n\r]+"))
             (hint (if grep-p "Grep file in %s: " "Find file in %s: "))
             selected-line
             selected-file
             linenum)
        (setq selected-line  (ivy-read (format hint default-directory)
                                          lines))
        (cond
          (grep-p
           (when (string-match "\\(^[^:]*\\):\\([0-9]*\\):" selected-line)
             (setq selected-file (match-string 1 selected-line))
             (setq linenum (match-string 2 selected-line))))
          (t
           (setq selected-file selected-line)))
        (when (and selected-line (file-exists-p selected-file))
          (find-file selected-file)
          (when grep-p
          (goto-char (point-min))
          (forward-line (1- (string-to-number linenum)))))
        ))))

(defun my-search-text-in-project ()
  "Search text of files in project root directory."
  (interactive)
  (my-file-internal (locate-dominating-file default-directory ".git") t))

(defun my-search-text (&optional level)
  "Search text of files in current directory or LEVEL parent directory."
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
    ;;2,xf
    (my-file-internal parent-directory t)))
;; M-x eval-buffer
;; M-x my-search-text-in-project
;; C-u 2 M-x my-search-text

再提速my-search-text

站在巨人的肩膀上,充分利用专业的工具。

  • repgrep https://github.com/BurntSushi/ripgrep
  • git grep
    • git grep -P -n -w '[A-Z]+_SUSPEND' 输出格式和 grep 一样
      • - P perl 正则
      • -n 显示行号
      • -w 单词匹配
      • git grep -n 'xxx'
    • 只能搜索有版本控制的文件
(defun my-file-internal (directory &optional grep-p git-grep-p)
...
             (git-cmd-fmt (if git-grep-p "git grep -n \"%s\"" "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." ))
             (grep-cmd (format git-cmd-fmt keyword))
...

完整内容:

(require 'ivy) ; optional
(defun my-file-internal (directory &optional grep-p git-grep-p)
  "Find file in DIRECTORY.
If GREP-P is t, grep files.
If GREP-P is nil, find files."
  (let* ((keyword (read-string "Please input keyword: ")))
    (when (and keyword (not (string= keyword "")))
      (let* ((find-cmd (format "find . \\( -path \"*/.git\" -o -path \"*/.svn\"  \\) -prune -o -type f -iname \"*%s*\" -print" keyword))
             (git-cmd-fmt (if git-grep-p "git grep -n \"%s\"" "grep --exclude-dir=\"*/.git\" --exclude-dir=\"*/.svn\"  -rns  \"%s\" ." ))
             (grep-cmd (format git-cmd-fmt keyword))
             (default-directory directory)
             (output (shell-command-to-string (if grep-p grep-cmd find-cmd)))
             (lines (split-string output "[\n\r]+"))
             (hint (if grep-p "Grep file in %s: " "Find file in %s: "))
             selected-line
             selected-file
             linenum)
        (setq selected-line  (ivy-read (format hint default-directory)
                                          lines))
        (cond
          (grep-p
           (when (string-match "\\(^[^:]*\\):\\([0-9]*\\):" selected-line)
             (setq selected-file (match-string 1 selected-line))
             (setq linenum (match-string 2 selected-line))))
          (t
           (setq selected-file selected-line)))
        (when (and selected-line (file-exists-p selected-file))
          (find-file selected-file)
          (when grep-p
          (goto-char (point-min))
          (forward-line (1- (string-to-number linenum)))))
        ))))

(defun my-git-search-text-in-project ()
  "Search text of files in project root directory using git grep."
  (interactive)
  (my-file-internal (locate-dominating-file default-directory ".git") t t))

(defun my-search-text-in-project ()
  "Search text of files in project root directory."
  (interactive)
  (my-file-internal (locate-dominating-file default-directory ".git") t))

(defun my-search-text (&optional level)
  "Search text of files in current directory or LEVEL parent directory."
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
    ;;2,xf
    (my-file-internal parent-directory t)))

(defun my-find-file-in-project ()
  "Find files in project root directory."
  (interactive)
  (my-file-internal (locate-dominating-file default-directory ".git")))

(defun my-find-file (&optional level)
  "Find file in current directory or LEVEL parent directory."
  (interactive "P")
  (unless level (setq level 0))
  ;; default-directory
  (let* ((parent-directory default-directory)
         (i 0))
    (while (< i level)
      ;; directory-file-name
      ;; file-name-directory
      (setq parent-directory
            (file-name-directory (directory-file-name parent-directory)))
      (setq i (+ i 1)))
    ;;2,xf
    (my-file-internal parent-directory)))
;; M-x eval-buffer
;; M-x my-search-text-in-project

搜索文件提速 利用 git 让 my-find-file-in-project 函数提速 `git ls-files –full-name –`

> git ls-files --full-name --
Jenkinsfile
README.md
addapay/addapay-jenkinsfile
app/app-jenkinsfile
app/app-website-jenkinsfile
bigdata/bigdata-jenkinsfile

不写任何代码大大加速文本文件操作 vmtouch

任何读写文件的软件都可以用这个方法来优化。如 firefox\chrome

vmtouch - the virtual memory toucher 把对应的文件或目录载入到内存中,不支持windows。 https://hoytech.com/vmtouch/

vmtouch 安装:

brew install vmtouch

常用选项: vmtouch [OPTIONS] .. FILES OR DERECTORIES … -v 选项来显示当前已经加入到内存中的文件或目录,以及它们所占用的内存大小 -t 指定目录载入到内存 -e 从内存中去除,下次从硬盘上读取。

测试对比:time

# grep
time grep --exclude='.git' -nrs 'test' * &> /dev/null

# 使用 vmtouch 工具将目录载入到内存
vmtouch -t .

# grep
time grep --exclude='.git' -nrs 'test' * &> /dev/null

#查已经加入到内存中的文件和目录
vmtouch -v /

#从内存中去除
vmtouch -e .

代码自动完成引擎company-mytags原型

创建文件:~/projs/company-my-tags/company-my-tags.el

模块加载

(message "hello world")

(provide 'company-mytags) ;; 告诉 emacs 提供 company-mytags 模块
;;; company-my-tags.el ends here

如果配置要载入模块,添加如下到配置中:

(require 'company-mytags)

模块位置是通过向 load-path 变量中加入路径。`C-h v load-path`

company-mode 第三方自动完成框架借鉴

UI 选项使用第三方 company-mode 提供的。http://company-mode.github.io/ 通过 `C-h C-f company-mode 打开源代码文件`

自动完成引擎:

C-h v company-backends

  • company-ispell 到英文词典中查英文单词
  • company-c-headers 自动找到 C 代码头文件
  • company-cmake 写 C++ cmake 脚本时自动完成
  • company-bbdb 写邮件时自动完成邮件地址、收件人名
  • company-semantic java 的
  • company-cmake
  • company-capf
  • company-clang
  • company-files 自动完成文件路径。如 `(cd "~/tmp")`
  • company-dabbrev-code 从已写代码中找到,自动完成
  • company-gtags 合命令行工具 gtags 结合,对 c++ java 支持比较好
  • company-ctags 类似于 gtags,智能程度没 gtags 高
  • company-keywords
  • company-oddmuse
  • company-dabbrev)

C-h C-f company-gtags 代码复制过来进行修改。

;;;###autoload
(defun company-gtags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (and (company-gtags--executable)
                 buffer-file-name
                 (apply #'derived-mode-p company-gtags-modes)
                 (not (company-in-string-or-comment))
                 (company-gtags--tags-available-p)
                 (or (company-grab-symbol) 'stop)))
    (candidates (company-gtags--fetch-tags arg))
    (sorted t)
    (duplicates t)
    (annotation (company-gtags--annotation arg))
    (meta (get-text-property 0 'meta arg))
    (location (get-text-property 0 'location arg))
    (post-completion (let ((anno (company-gtags--annotation arg)))
                       (when (and company-gtags-insert-arguments anno)
                         (insert anno)
                         (company-template-c-like-templatify anno))))))

(provide 'company-gtags)
;;; company-gtags.el ends here

prefix: 不是函数,返回用户输入的前缀给 framework

company-grab-symbol: 当前的符号

candidates:对用户输入前缀处理, 把用户输入作为参数传给你,返回一个列表,列表中包含字符串,这些字符串就是用来自动完成的。

只保留自定义引擎

(defun company-mytags-candidates (arg)
  (message "arg=%s" arg)
  '("hello1"
    "hello2"
    "bye1"
    "bye2"))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here

测试:

输入 test 可以看到能匹配出 company-mytags-candidates 函数给定的字符。

添加字符串过滤功能

push: (push NEWELT PLACE) 列表中追加元素。

(defun company-mytags-candidates (prefix)
  (let* ((all-items '("hello1"
                     "hello2"
                     "test1"
                     "testtab2"
                     "testtab3"
                     "bye1"
                     "bye2"))
         rlt)
    (dolist (item all-items)
      (when (string-match prefix item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here

company-mode 默认至少在用户输入 3 个字符时才会触发自动实例功能。自定义可以修改变量 company-minimum-prefix-length。

为company-mytags开发模糊(fuzzy)

正则表达式稍微灵活运用一下。

company-backends 默认是局部变量,我们可以在源代码中找到定义:

(put 'company-backends 'safe-local-variable 'company-safe-backends-p)

safe-local-variable: 表示变量和你当前打开的 buffer 绑定的。

只要在新的 buffer 中运行自动补全引擎就可以了。

;; M-x eval-expression (setq company-backends '(company-mytags))

匹配行的开始

(when (string-match (concat "^" prefix) item)

fuzzy match 只要第一个字符能匹配,后面的字符能匹配到。 如: hello 可以写为 ^h.*l.*l

取字符串的字符:(substring STRING &optional FROM TO) 下标 0 为第一个字符。

  • (substring "abc" 0 1) 取第一个字符 a
(defun company-mytags-candidates (prefix)
  (let* ((all-items '("hello1"
                     "hello2"
                     "test1"
                     "testtab2"
                     "testtab3"
                     "bye1"
                     "bye2"))
         (i 0)
         (pattern "^")
         rlt)
    ;; hel => ^h.*e.*l.*
    (while (< i (length prefix))
      (setq pattern (concat pattern (substring prefix i (1+ i)) ".*"))
      (setq i (1+ i)))

    (message "pattern=%s" pattern)

    (dolist (item all-items)
      (when (string-match pattern item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here

在其他IDE应用基于模糊匹配算法的技巧

可以模糊匹配。如 cpp test

  • c.*p.*p.*t.*e.*s.*t.*
  • t.*e.*s.*t.*c.*p.*p.*

让company-mytags可以实时扫描项目代码

扫描真实目录来提取后选字符串。

  • 找到根目录
  • 找文件
(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory ".git"))
         (find-cmd (format "find . -path \"*/.git\"  -prune -o -type f -iname \"*.html*\" -print"))
         (output (shell-command-to-string find-cmd))
         (files (split-string output "[\n\r]+"))
         rlt)
    (message "root-dir=%s" root-dir)
    (message "files=%s" lines)
    (setq rlt '("hello1"
                      "hello2"
                      "test1"
                      "testtab2"
                      "testtab3"
                      "bye1"
                      "bye2"))
    rlt))

读出文件内容

读文件 insert-file-contents filename

(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory ".git"))
         (find-cmd (format "find . -path \"*/.git\"  -prune -o -type f -iname \"*.html*\" -print"))
         (output (shell-command-to-string find-cmd))
         (files (split-string output "[\n\r]+"))
         (all-content "")
         rlt)
    (message "root-dir=%s" root-dir)
    (message "files=%s" files)
    (dolist (file files)
      (when (and (not (string= file ""))
                 (file-exists-p file))
        (message "file=%s" file)
        (setq all-content (concat all-content
                                  (with-temp-buffer
                                    (insert-file-contents file)
                                    (buffer-string)
                                    )))))
    (message "all-content=%s  len=%s"
             (substring all-content 0 100)
             (length all-content))
    ;; scan root-dir
    (setq rlt '("hello1"
                "hello2"
                "test1"
                "testtab2"
                "testtab3"
                "bye1"
                "bye2"))
    rlt))

取出文件内容

  • 正则以空格或者非字符分隔:(split-string str "[^a-zA-Z-_]")
  • 对字符串进行排序:(sort '("abc" "e" "c") 'string<)
  • 从列表中删除重复项: (delq nil (delete-dups (list "foo" "bar" nil "moo" "bar" "moo" nil "affe"))) 参考 stack 回答
(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory ".git"))
         (find-cmd (format "find . -path \"*/.git\"  -prune -o -type f -iname \"*.html*\" -print"))
         (output (shell-command-to-string find-cmd))
         (files (split-string output "[\n\r]+"))
         (all-content "")
         rlt)
    (dolist (file files)
      (when (and (not (string= file ""))
                 (file-exists-p file))
        (setq all-content (concat all-content (with-temp-buffer
                                                (insert-file-contents file)
                                                (buffer-string))))))
    ;; scan root-dir
    (setq rlt (delq nil (delete-dups (sort (split-string all-content "[^a-zA-Z-_]") 'string<))))
    rlt))

(defun company-mytags-candidates (prefix)
  (let* ((all-items (company-mytags-scan-project-root))
         (i 0)
         (pattern "^")
         rlt)
    (while (< i (length prefix))
      (setq pattern (concat pattern (substring prefix i (1+ i)) ".*"))
      (setq i (1+ i)))

    (dolist (item all-items)
      (when (string-match pattern item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here
;; M-x eval-buffer

缺点:

  • 性能差,敲入一个字符都会调用这个函数,读取多文件。
  • emacs lisp 性能比较低,它不是专门用来文件高效读写与编程。一般会用 c 或者 c++ 或者其它语言来做
  • 没有扩展性,对多语言不适用

利用ctags来加强company-mytags的代码扫描

ctags 安装

ctags 支持各种编程语言。

官方地址:https://ctags.sourceforge.net/

安装 参考:https://galea.medium.com/getting-started-with-ctags-vim-on-macos-87bcb07cf6d

# macos
brew install ctags
alias ctags="`brew --prefix`/bin/ctags"
alias ctags >> ~/.bashrc

ctags --version
man ctags

ctags 命令 选项

-e 生成 emacs 兼容的格式
-R 递归搜索子目录
--exclude 排除文件

ctags 使用

# 生成 TAGS 文件
ctags -e -R

cat TAGS
^L # 换页
company-my-tags.el,238 #文件名,文件大小
# 列出扫描出来的关键字 ^?符号 ^? 行号
(defun company-mytags-scan-project-root ()^?company-mytags-scan-project-root^A1,0
(defun company-mytags-candidates (prefix)^?company-mytags-candidates^A18,798
(defun company-mytags (command &optional arg &rest ignored)^?company-mytags^A32,1182


# 演示排除用法
mkdir subdir1
touch subdir1/hello.el
cat subdir1/hello.el
(defun helle ()
  (message "hello world"))
(defun bye ()
  (message "bye world"))

ctags -e -R
> cat TAGS
^L
company-my-tags.el,238
(defun company-mytags-scan-project-root ()^?company-mytags-scan-project-root^A1,0
(defun company-mytags-candidates (prefix)^?company-mytags-candidates^A18,798
(defun company-mytags (command &optional arg &rest ignored)^?company-mytags^A32,1182
^L
subdir1/hello.el,49
(defun helle ()^?helle^A1,0
(defun bye ()^?bye^A3,43


ctags -e -R --exclude="subdir1/*"
> cat TAGS
^L
company-my-tags.el,238
(defun company-mytags-scan-project-root ()^?company-mytags-scan-project-root^A1,0
(defun company-mytags-candidates (prefix)^?company-mytags-candidates^A18,798
(defun company-mytags (command &optional arg &rest ignored)^?company-mytags^A32,1182

# 排除当前目录下目录,和任意目录下子目录文件
ctags -e -R --exclude="subdir1/*" --exclude="*/subdir1/*" 
ctags -e -R --exclude=".git/*" --exclude="*/.git/*" --exclude=".#*"   # .#* 过滤 .#xxx 防系统崩溃的隐藏文件

ctags 定时任务

ctags 扫描时还是影响性能的,我们可以利用定时任务来执行。如在 linux 下的 crontab

cd /Users/7tq6lr/projs/company-my-tags &&  ctags -e -R --exclude=".git/*" --exclude="*/.git/*" --exclude=".#*"   # .#* 过滤 .#xxx 防系统崩溃的隐藏文件

mytags

  • 只需要找到 TAGS 所在目录
  • 不需要执行命令
  • TAGS 文件内容用 8 进制分隔处理。
    • 按 ga 显示字符的 ASCII 码 10 进制、8 进制、16进制数
    • ^? 8 进制为 177,^A 8 进制为 1

修改 company-mytags-scan-project-root 函数

(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         ;; ctags -e -R --exclude=".git/*" --exclude="*/.git/*"
         all-content
         (regex "^[^\177\001]+\177\\([^\177\001]+\\)\001[0-9]+,[0-9]+$")
         (start 0)
         tag-name
         rlt)
    ;; (message "tags-files=%s" tags-file)
    (when (and (file-exists-p tags-file)
               (setq all-content (with-temp-buffer
                                   (insert-file-contents tags-file)
                                   (buffer-string))))
      (while (string-match regex all-content start)
        (setq tag-name (match-string 1 all-content))
        ;; (message "----rlt=%s" rlt)
        (push tag-name rlt)
        (setq start (+ start (length (match-string 0 all-content))))
        )
      )
    ;; (message "rlt=%s" rlt)
    (setq rlt (delq nil (delete-dups (sort rlt 'string<))))
    rlt))

不是每次都执行。

  • 定义缓存变量 company-mytags-cache
  • 没缓存时调用 company-mytags-scan-project-root 函数,有缓存时直接使用缓存中的值

仅修改 company-mytags-candidates 函数

(defvar company-mytags-cache nil
  "Cache of tags fiel")

(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         ;; ctags -e -R --exclude=".git/*" --exclude="*/.git/*"
         all-content
         (regex "^[^\177\001]+\177\\([^\177\001]+\\)\001[0-9]+,[0-9]+$")
         (start 0)
         tag-name
         rlt)
    (when (and (file-exists-p tags-file)
               (setq all-content (with-temp-buffer
                                   (insert-file-contents tags-file)
                                   (buffer-string))))
      (while (string-match regex all-content start)
        (setq tag-name (match-string 1 all-content))
        (push tag-name rlt)
        (setq start (+ start (length (match-string 0 all-content))))))
    (setq rlt (delq nil (delete-dups (sort rlt 'string<))))
    rlt))

(defun company-mytags-candidates (prefix)
  (let* ((i 0)
         (pattern "^")
         rlt)
    ;; file cache
    (unless company-mytags-cache
      (setq company-mytags-cache (company-mytags-scan-project-root)))

    ;; matching by prefix
    (while (< i (length prefix))
      (setq pattern (concat pattern (substring prefix i (1+ i)) ".*"))
      (setq i (1+ i)))

    ;; finalize candidates
    (dolist (item company-mytags-cache)
      (when (string-match pattern item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here

company-mytags可实时载入Ctags生成

性能优化可以处理大型项目。需要一些新的算法来处理。

(defvar company-mytags-cache nil  "Cache of tags fiel")

(defun company-mytags-update-p (tags-file)
  (let* (rlt old-modification-time file-modification-time)
    (cond
     ((not company-mytags-cache)
      (setq rlt t))
     (t
      (setq old-modification-time (plist-get company-mytags-cache
                                             'modification-time))
      (setq file-modification-time
            (float-time (nth 5 (file-attributes tags-file))))
      (message "old=%s file=%s" old-modification-time file-modification-time)
      (message "t=%s"  (- file-modification-time old-modification-time))
      (when (> (- file-modification-time old-modification-time) 4)
        (setq rlt t))))
    rlt))

(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         ;; ctags -e -R --exclude=".git/*" --exclude="*/.git/*"
         all-content
         (regex "^[^\177\001]+\177\\([^\177\001]+\\)\001[0-9]+,[0-9]+$")
         (start 0)
         tag-name
         rlt)
    (when (company-mytags-update-p tags-file)
      ;; attach file modification time
      (setq company-mytags-cache
            (plist-put company-mytags-cache
                       'modification-time
                       (float-time (nth 5 (file-attributes tags-file)))))
      (message "company-mytags-cache=%s" company-mytags-cache)
      (when (and (file-exists-p tags-file)
                 (setq all-content (with-temp-buffer
                                     (insert-file-contents tags-file)
                                     (buffer-string))))
        ;; extract tag names
        (while (string-match regex all-content start)
          (setq tag-name (match-string 1 all-content))
          (push tag-name rlt)
          (setq start (+ start (length (match-string 0 all-content))))))

      ;; update candidates
      (setq company-mytags-cache
            (plist-put company-mytags-cache
                       'candidates
                       (delq nil (delete-dups (sort rlt 'string<)))))

      (message "company-mytags-cache=%s" company-mytags-cache)
      )))

(defun company-mytags-candidates (prefix)
  (let* ((i 0)
         (pattern "^")
         rlt)
    ;; file cache
    (company-mytags-scan-project-root)

    ;; matching by prefix
    (while (< i (length prefix))
      (setq pattern (concat pattern (substring prefix i (1+ i)) ".*"))
      (setq i (1+ i)))

    ;; finalize candidates
    (dolist (item (plist-get company-mytags-cache 'candidates))
      (when (string-match pattern item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for GNU Global."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-gtags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

(setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-my-tags.el ends here

;; M-x eval-buffer
;; 清空缓存 (setq company-mytags-cache nil)
;; 本目录下打开个新文件,M-x eval-expression (setq company-backends '(company-mytags))

优化company-mytags性能

如何在大项目中使用 company-mytags。 在大项目中如linux内核代码,存在 TAGS 文件在生成过程中可能被加载现象。

  • file-attributes filename 输出的第7个属性为文件大小
  • 新内容加入到老缓存中
    • diff 命令比较文本文件 diff -Nabur TAGS TAGS.2
    • 生产临时文件:(make-temp-file PREFIX &optional DIR-FLAG SUFFIX TEXT)
    • 写入文件:(write-region "something something" nil "~/temp.txt")

shell 包使用

M-x shell 命令打开 Shell 窗口

C-c C-c 中断正在运行的命令。
C-c C-z 将当前命令挂起,并返回到 Emacs 缓冲区。
C-c C-u 清空当前命令行。

M-p 直到找到需要的命令
M-n 查找之前执行的命令

完成内容

(defvar company-mytags-cache nil "Cache of tags file")

(defun company-mytags-update-p (tags-file)
  (let* (rlt old-modification-time file-modification-time old-file-size file-size)
    (cond
     ((not company-mytags-cache)
      (setq rlt "full-update"))
     (t
      (setq old-file-size (plist-get company-mytags-cache
                                     'file-size))
      (setq file-size (nth 7 (file-attributes tags-file)))
      (setq old-modification-time (plist-get company-mytags-cache
                                             'modification-time))
      (setq file-modification-time
            (float-time (nth 5 (file-attributes tags-file))))
      (message "old-file-size=%s file-size=%s" old-file-size file-size)
      (when (and (> (- file-modification-time old-modification-time) 4)
                 (> file-size old-file-size))
        (setq rlt "lite-update"))))
    rlt))

(defun company-mytags-extract-cands (regex content)
  (let* (rlt (start 0) tag-name)
    (while (string-match regex content start)
      (setq tag-name (match-string 1 content))
      (push tag-name rlt)
      (setq start (+ start (length (match-string 0 content)))))
    (delq nil (delete-dups (sort rlt 'string<)))))

(defun company-mytags-scan-project-root ()
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         ;; ctags -e -R --exclude=".git/*" --exclude="*/.git/*"
         all-content
         (regex "^[^\177\001]+\177\\([^\177\001]+\\)\001[0-9]+,[0-9]+$")
         do-update
         rlt)
    (when (setq do-update (company-mytags-update-p tags-file))
      ;; attache file path
      (setq company-mytags-cache
            (plist-put company-mytags-cache
                       'file-path
                       (file-truename tags-file)))
      ;; attach file modification time
      (setq company-mytags-cache
            (plist-put company-mytags-cache
                       'modification-time
                       (float-time (nth 5 (file-attributes tags-file)))))

      ;; attach file size
      (setq company-mytags-cache
            (plist-put company-mytags-cache
                       'file-size
                       (nth 7 (file-attributes tags-file))))
      ;;  load content of tags file into memory
      (cond
       ;; full update
       ((string= do-update "full-update")
        (message "run full update")
        (setq all-content (with-temp-buffer
                            (insert-file-contents tags-file)
                            (buffer-string)))

        ;; attach file content
        (setq company-mytags-cache
              (plist-put company-mytags-cache
                         'file-content
                         all-content))

        ;; extract tag names
        (setq rlt (company-mytags-extract-cands regex all-content))
        ;; insert candidates to empty list
        (setq company-mytags-cache
              (plist-put company-mytags-cache
                         'candidates
                         rlt)))

       ;; lite update
       ((string= do-update "lite-update")
        ;; run diff between old tags file and new tags file
        (message "run lite update")
        (let* ((tmp-file (make-temp-file "mytags"))
               (old-content (plist-get company-mytags-cache 'file-content))
               (old-candidates (plist-get company-mytags-cache 'candidates))
               diff-output)
          (write-region old-content nil tmp-file)
          ;; make sure diff exist in $PATH
          (setq diff-output (shell-command-to-string (format "diff -Nabur %s %s"
                                                             tmp-file
                                                             tags-file)))
          ;; extract tag names
          (setq rlt (company-mytags-extract-cands regex diff-output))
          (message "lite update rlt=%s" rlt)
          ;; insert candidates to empty list
          (setq company-mytags-cache
                (plist-put company-mytags-cache
                           'candidates
                           (append rlt old-candidates)))))))))

(defun company-mytags-candidates (prefix)
  (let* ((i 0)
         (pattern "^")
         rlt)
    ;; fill cache
    (company-mytags-scan-project-root)

    ;; matching by prefix
    (while (< i (length prefix))
      (setq pattern (concat pattern (substring prefix i (1+ i)) ".*"))
      (setq i (1+ i)))

    (message "candidates size=%s" (length (plist-get company-mytags-cache 'candidates)))
    ;; finalize candidates
    (dolist (item (plist-get company-mytags-cache 'candidates))
      (when (string-match pattern item)
        (push item rlt)))
    rlt))

(defun company-mytags (command &optional arg &rest ignored)
  "`company-mode' completion backend for mytags."
  (interactive (list 'interactive))
  (cl-case command
    (interactive (company-begin-backend 'company-mytags))
    (prefix (company-grab-symbol))
    (candidates (company-mytags-candidates arg))))

;; (setq company-backends '(company-mytags)) ;; debug code
(provide 'company-mytags)
;;; company-mytags.el ends here

验证

#加载代码
M-x eval-buffer


#打开多容器,并打开一个代码库文件
#打开shell终端,生产 TAGS 文件
ctags -e -R --exclude=".git/*" --exclude="*/.git/*"
vmtouch -t . # 载入内存加速

# 加载后端引擎
M-x eval-expression (setq company-backends '(company-mytags))

#验证1-第一次,慢
# 清空缓存
M-x eval-expression (setq company-mytags-cache nil)
文件中输入任意函数名前缀
#验证2-修改 TAGS 文件,增加些函数
文件中输入任意函数名前缀

代码导航插件my-codenav原型

支持所有流行编程语言。

创建代码导航目录code navigation

mkdir ~/projs/my-codenav

创建一些js代码

> cat bye.js
function bye1() {
}

function bye2() {
}

> cat hello.js
function hello1() {
}

function hello2() {
}%

ctags命令生效关键词

ctags -e -R *.js

创建lisp代码文件

emacs -nw my-codenav.el

相对目录

  • default-directory:is a buffer-local variable defined in C source code. 每一个 buffer 都有这个变量,当前绑定的目录。
    • 相对路径:./abc/def 相对路径对于任何系统都适用
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         (default-directory root-dir))
    (message "my-codenav called. TAGS path=%s default-directory=%s" tags-file default-directory)))

;;M-x eval-buffer
;;M-x my-codenav 输出:my-codenav called. TAGS path=~/projs/my-codenav/TAGS default-directory=~/projs/my-codenav/

算法

  • 算法:从TAGS文件开头向搜索,搜到关键函数,如bye1,找到行号再找到对应的文件。
  • 找到TAGS中函数所在的文件名,(etags-file-of-tag t)
  • 当要调用一个函数时要在 temp 里操作,with-temp-buffer
  • 当前buffer搜索 (search-forward "xx")
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         all-content
         (default-directory root-dir))

    (setq all-content (with-temp-buffer
                        (insert-file-contents tags-file)
                        (buffer-string)))
    (with-temp-buffer
      (insert all-content)
      (goto-char (point-min))
      (search-forward "bye1")
      (message "file=%s" (etags-file-of-tag t)))
    ))

验证

#多开容器打开 bye.js文件,M-x my-codenav
  • 显示光标所在符号的名称 (thing-at-point 'symbol)
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         (symbol (thing-at-point 'symbol))
         all-content
         cur-line
         (regex "^\\([^\177\001]+\\)\177\\([^\177\001]+\\)\001\\([0-9]+\\),[0-9]+$")
         (default-directory root-dir))

    (setq all-content (with-temp-buffer
                        (insert-file-contents tags-file)
                        (buffer-string)))
    (when (and symbol
               (not (string= symbol "")))
      (with-temp-buffer
        (insert all-content)
        (goto-char (point-min))
        (search-forward symbol)
        (setq cur-line (buffer-substring (line-beginning-position) (line-end-position)))
        (when (string-match regex cur-line)
          (let* ((code-line (match-string 1 cur-line))
                 (code-num (match-string 3 cur-line))
                 (file (etags-file-of-tag t)))
            (message "code-line=%s" code-line)
            (message "code-num=%s" code-num)
            (message "file=%s" file)
            ))
        ;;(message "file=%s" (etags-file-of-tag t))
        )
      )))
  • 处理多文件同名函数问题
  • 使用正则匹配定位每行的函数关键词,而不是用 search-forward 搜索,因为一行里包含2个同名词
    • (search-forward-regexp (concat "\177" symbol "\001") (point-max) t)
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         (symbol (thing-at-point 'symbol))
         candidates ;; 等价于nil '()
         all-content
         cur-line
         (regex "^\\([^\177\001]+\\)\177\\([^\177\001]+\\)\001\\([0-9]+\\),[0-9]+$")
         (default-directory root-dir))

    (setq all-content (with-temp-buffer
                        (insert-file-contents tags-file)
                        (buffer-string)))
    (when (and symbol
               (not (string= symbol "")))
      (with-temp-buffer
        (insert all-content)
        (goto-char (point-min))
        (while (search-forward-regexp (concat "\177" symbol "\001") (point-max) t)
          (setq cur-line (buffer-substring (line-beginning-position) (line-end-position)))
          (when (string-match regex cur-line)
            (let* ((code-line (match-string 1 cur-line))
                   (code-num (string-to-number (match-string 3 cur-line)))
                   (file (etags-file-of-tag t)))
              (push (list code-line code-num file) candidates)))
         (forward-line)))
      (message "candidates=%s" candidates))))
  • 引用查找文件部分代码
(require 'ivy)
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         (symbol (thing-at-point 'symbol))
         candidates ;; 等价于nil '()
         all-content
         cur-line
         selected
         (regex "^\\([^\177\001]+\\)\177\\([^\177\001]+\\)\001\\([0-9]+\\),[0-9]+$")
         (default-directory root-dir))
    (setq all-content (with-temp-buffer
                        (insert-file-contents tags-file)
                        (buffer-string)))
    (when (and symbol (not (string= symbol "")))
      (with-temp-buffer
        (insert all-content)
        (goto-char (point-min))
        (while (search-forward-regexp (concat "\177" symbol "\001") (point-max) t)
          (setq cur-line (buffer-substring (line-beginning-position) (line-end-position)))
          (when (string-match regex cur-line)
            (let* ((code-line (match-string 1 cur-line))
                   (line-num (match-string 3 cur-line))
                   (file (etags-file-of-tag t))
                   one-candidate)
              (setq one-candidate (format "%s:%s:%s" file line-num code-line))
              (push one-candidate candidates)))
          (forward-line)))
      (message "candidates=%s" candidates)
      (when (and candidates (setq selected (ivy-read "Naviget to: " candidates)))
        (when (string-match "^\\([^:]+\\):\\([0-9]+\\):.*" selected)
          (let* ((file (match-string 1 selected))
                 (line-num (match-string 2 selected)))
            (message "file=%s line=%s" file line-num)
            (when (and file (file-exists-p file))
              (find-file file)
              (goto-char (point-min))
              (forward-line (1- (string-to-number line-num))))))))))

改善my-codenav用户体验

  • 支持跳回
    • 官方代码导航插件:(require 'xref)
    • 标记:(xref-push-marker-stack (point-marker))
    • 跳回: pop-tag-mark C-t
    • 跳回后闪烁: (xref-pulse-momentarily)
(require 'ivy)
(require 'xref)
(defun my-codenav ()
  "My code navigation tool."
  (interactive)
  (let* ((root-dir (locate-dominating-file default-directory "TAGS"))
         (tags-file (concat root-dir "TAGS"))
         (symbol (thing-at-point 'symbol))
         candidates ;; 等价于nil '()
         all-content
         cur-line
         selected
         (regex "^\\([^\177\001]+\\)\177\\([^\177\001]+\\)\001\\([0-9]+\\),[0-9]+$")
         (default-directory root-dir))
    (setq all-content (with-temp-buffer
                        (insert-file-contents tags-file)
                        (buffer-string)))
    (when (and symbol (not (string= symbol "")))
      (with-temp-buffer
        (insert all-content)
        (goto-char (point-min))
        ;; push all matches into candidates
        (while (search-forward-regexp (concat "\177" symbol "\001") (point-max) t)
          (setq cur-line (buffer-substring (line-beginning-position) (line-end-position)))
          (when (string-match regex cur-line)
            (let* ((code-line (match-string 1 cur-line))
                   (line-num (match-string 3 cur-line))
                   (file (etags-file-of-tag t)))
              (push (format "%s:%s:%s" file line-num code-line) candidates)))
          (forward-line)))
      (when (and candidates (setq selected (ivy-read "Naviget to: " candidates)))
        (when (string-match "^\\([^:]+\\):\\([0-9]+\\):.*" selected)
          (let* ((file (match-string 1 selected))
                 (line-num (match-string 2 selected)))
            (message "file=%s line=%s" file line-num)
            (when (and file (file-exists-p file))
              (xref-push-marker-stack (point-marker))
              (find-file file)
              (goto-char (point-min))
              (forward-line (1- (string-to-number line-num)))
              (xref-pulse-momentarily))))))))

打造语法检查器my-syntax-check

创建代码目录

mkdir ~/projs/my-syntax-check

emacs 选项

-l --load 载入文件
-batch 不交互式显示,直接输出到控制台
-Q 快速输出

范例: 检查每一行不能有多余空格

# hello.txt
hello world.
hello world.
hello world.
hello world.
hello world.
bye world.
bye world.
> cat my-syntax-checker.el
(message "hello world")
> e -Q -batch -l my-syntax-checker.el
hello world
  • 变量comand-line-args-left,存储命令参数
> cat my-syntax-checker.el
(message "hello world")
(message "command-line-args-left=%s" command-line-args-left)


> emacs -Q -batch -l my-syntax-checker.el hello.txt
hello world
command-line-args-left=(hello.txt)
my-syntax-checker:hello.txt:3:space at the end of line
my-syntax-checker:hello.txt:6:space at the end of line
  • 读入文件,再一行一行读出文件内容
  • 检查每行尾有空格时就报错,匹配行尾空格`" +$"`
(let* ((file (nth 0 command-line-args-left))
       (i 0)
       lines
       all-content)
(when (file-exists-p file)
  (setq all-content (with-temp-buffer
                      (insert-file-contents file)
                      (buffer-string)))
  (setq lines (split-string all-content "\n"))
  (while (< i (length lines))
    (when (string-match " +$" (nth i lines))
      (message "my-syntax-checker:%s:%s:space at the end of line"
               file
               (1+ i)))
    (setq i (1+ i)))))


> emacs -Q -batch -l my-syntax-checker.el hello.txt
my-syntax-checker:hello.txt:3:space at the end of line
my-syntax-checker:hello.txt:6:space at the end of line
  • `~/.custom.el`添加 `(setq lazyflymake-update-interval 999999)` 用原始的flymake

my-flymake-txt.el中设置flymake

  • 定义init函数
  • 变量buffer-file-name 当前buffer的文件路径
  • 新正则表达式语法 "\\.txt$" 老的 "\\.txt\\'"
  • 将"-Q -batch -l my-syntax-checker.el hello.txt" 推送到变量 flymake-allowed-file-name-masks中
  • 抓到的输出信息追加到变量 flymake-err-line-patterns中,第一个是文件名,第二个是行号,第3个类号,第4个
  (list "emacs"
        (list "-Q"
              "-batch"
              "-l"
              "/Users/7tq6lr/projs/my-syntax-check/my-syntax-checker.el"
              buffer-file-name)))
(push (list "\\.txt$" 'my-flymake-txt-init) flymake-allowed-file-name-masks)
;;my-syntax-checker:hello.txt:3:space at the end of line
(push (list "^my-syntax-checker:\\([^:]+\\):\\([0-9]+\\):\\(.*\\)$" 1 2 nil 3)
      flymake-err-line-patterns)

M-x eval-buffer 加载代码,在新窗口中打开hello.txt文件,开启flymake-mode,可以看到自动检查出语法问题了

打造单词拼写检查器my-spellcheck

emacs 自带一个flyspell-mode拼写检查插件,这里会用它的一些api打造一个自己的拼写检查插件,可以检查字符串的拼写、注释的拼写、函数拼写

  • 关闭作者的无错插件,在 ~/.custom.el 添加 (setq wucuo-update-interval 999999)
  • GNU Aspell 拼写检查工具默认支持驼峰变量命名 http://aspell.net/

ubuntu上安装aspell包

sudo apt install aspell aspell-en
#mac
brew install aspell

aspell使用

/Users/7tq6lr/projs/my-syntax-check [7tq6lr@7tq6lrdeMacBook-Air] [6:40]
> echo "hello world" | aspell pipe --lang en
@(#) International Ispell Version 3.1.20 (but really Aspell 0.60.8)
*
*


/Users/7tq6lr/projs/my-syntax-check [7tq6lr@7tq6lrdeMacBook-Air] [6:41]
> echo "helle world" | aspell pipe --lang en
@(#) International Ispell Version 3.1.20 (but really Aspell 0.60.8)
& helle 29 0: Heller, hell, hello, heel, he'll, helve, belle, Halley, Hallie, Holley, Hollie, healer, holler, huller, Hale, Hall, Hill, Hull, hale, hall, heal, hill, hole, hull, Holly, Hoyle, hilly, holly, hell's
*

# 支持驼峰拼写检查,0.60.8版本 完成支持
> echo "helloWorld" | aspell pipe --lang en --run-together
@(#) International Ispell Version 3.1.20 (but really Aspell 0.60.8)
-

创建代码文件

mkdir -p  ~/projs/my-spellcheck
cd ~/projs/my-spellcheck
touch my-spellcheck.el
cat my-spellcheck.el
(defun my-spell-check-start ()
  (interactive)
  (message "hello world"))



cat hello.js
function hello() {
  console.log('hello,world');
  console.log('hell,world');
}
  • flyspell-buffeer 扫描当前buffer
(require 'flyspell)
;; use `flyspell-buffer'
(setq ispell-program-name "aspell")
(setq ispell-extra-args '("--lang=en" "--run-together"))
(defun my-spell-check-start ()
  (interactive)
  (flyspell-buffer))

M-x eval-buffer M-x my-spell-check-start 可以看到 hello.js中有找到错误单词高亮

  • flyspell-generic-check-word-predicate 用户自定义变量
  • 保存文件时自动检查
    • (add-hook 'after-save-hook 'my-spell-check-start nil t)
(require 'flyspell)
;; use `flyspell-buffer'
(setq ispell-program-name "aspell")
(setq ispell-extra-args '("--lang=en" "--run-together"))
(defun my-predicate ()
  (let* ((pos (1- (point)))
         (font-face-at-point (get-text-property pos 'face)))
    (memq font-face-at-point '(font-lock-string-face
                               font-lock-comment-face
                               font-lock-function-name-face
                               font-lock-variable-name-face))
    ))

(defun my-spell-check-start ()
  (interactive)
  (setq flyspell-generic-check-word-predicate 'my-predicate)
  (flyspell-buffer))

;; (add-hook 'after-save-hook 'my-spell-check-start nil t)
(defun my-mode-hook-setup ()
  (add-hook 'after-save-hook 'my-spell-check-start nil t))
(add-hook 'js2-mode-hook 'my--mode-hook-setup)

优化my-spellcheck性能

大项目中的优化

  • 修改flyspell-buffer中代码,(flyspell-region (point-min) (point-max))
    • 缩小检查区域
      • 当前控制焦点是 (point)
      • 可见窗口的高度,即打开的buffer窗口肉眼能看到的高度(window-body-height)
      • 保留当前的焦点,并可执行一下代码,执行结束后回退到原焦点 save-excursion
      • begin 回退一个容器高度,取行首
      • end 向下
(require 'flyspell)
;; use `flyspell-buffer'
(setq ispell-program-name "aspell")
(setq ispell-extra-args '("--lang=en" "--run-together"))
(defun my-predicate ()
  (let* ((pos (1- (point)))
         (font-face-at-point (get-text-property pos 'face)))
    ;;; ...skip some logic .....
    (memq font-face-at-point '(font-lock-string-face
                               font-lock-comment-face
                               font-lock-variable-name-face
                               font-lock-function-name-face))))
(defun my-spell-check-start ()
  (interactive)
  (let* (begin end (height (window-body-height)))
    (message "my-spell-check-start called")
    (setq flyspell-generic-check-word-predicate 'my-predicate)
    ;; (point)
    (save-excursion
      (forward-line (- height))
      (setq begin (line-beginning-position)))
    (save-excursion
      (forward-line height)
      (setq end (line-end-position)))
    (flyspell-region begin end)))

;; (add-hook 'after-save-hook 'my-spell-check-start nil t)
(defun my-mode-hook-setup ()
  (add-hook 'after-save-hook 'my-spell-check-start nil t))
(add-hook 'js2-mode-hook 'my-mode-hook-setup)

文本操作神器text object

text object文本对象,vim文本也有对象概念,比如一个单词、句子、段落、tag、块(如括号、方括号、引号),通过操作文本对象来修改比只操作单个字符高效。

[number]<operator>[text object or motion]
<数字> <操作符> <文本对象或移动命令>

emacs中可以自己定义 text object

mkdir -p ~/projs/my-evil-textobj
cd ~/projs/my-evil-textobj
touch my-evil-textobj.el

cat hello.js
function bye() {
}
function hello() {
  let v1 = bye();
}
  • 利用 evil-define-text-object 宏定义中的函数

原始代码:

(require 'evil)

(evil-define-text-object evil-a-word (count &optional beg end type)
  "Select a word."
  (evil-select-a-restricted-object 'evil-word beg end type count))

(evil-define-text-object evil-inner-word (count &optional beg end type)
  "Select inner word."
  (evil-select-inner-restricted-object 'evil-word beg end type count))
(define-key evil-outer-text-objects-map "w" 'evil-a-word)
(define-key evil-inner-text-objects-map "w" 'evil-inner-word)
;;press `viw'evil-inner-word  `vaw' evil-a-word
;; "word" vi" va"
;; (word) vi( va(
;; i 不包括边界,a 包括边界
  • 验证输出
(require 'evil)

(evil-define-text-object my-evil-a-textobj (count &optional beg end type)
  "Select a word."
  (let* ((rlt (evil-select-a-restricted-object 'evil-word beg end type count)))
    (message "rlt=%s" rlt)
    (setq rlt '(28 34))
    rlt))

(evil-define-text-object my-evil-inner-textobj (count &optional beg end type)
  "Select inner word."
  (evil-select-inner-restricted-object 'evil-word beg end type count))
(define-key evil-outer-text-objects-map "t" 'my-evil-a-textobj)
(define-key evil-inner-text-objects-map "t" 'my-evil-inner-textobj)
;;press `viw'evil-inner-word  `vaw' evil-a-word

在hello.js中使用 vat 查看自定义的列表 28 34 一样可以使用。

text object 改造

  • 选中一行
    • (list (line-beginning-position) (line-end-position))
  • 开头去掉空格和TAB
    • asscii 32 9
    • 当前字符是什么 (following-char)
    • 光标移动到下一个字符 (forward-char)
  • 不包含边界
    • 找到 = 等号 asscii 61
    • 跳过 = 等号 和 ; 分号
(require 'evil nil t)
(defun my-skip-white-space (start step)
  "Skip white spaces from START, return position of first non-space character.
If STEP is 1,  search in forward direction, or else in backward direction."
  (let* ((b start)
         (e (if (> step 0) (line-end-position) (line-beginning-position))))
    (save-excursion
      (goto-char b)
      (while (and (not (eq b e)) (memq (following-char) '(9 32)))
        (forward-char step))
      (point))))

(evil-define-text-object my-evil-a-textobj (count &optional beg end type)
  "Select a statement."
  (list (my-skip-white-space (line-beginning-position) 1)
        (line-end-position)))

(evil-define-text-object my-evil-inner-textobj (count &optional beg end type)
  "Select inner statement."
  (let* ((b (my-skip-white-space (line-beginning-position) 1))
         (e (line-end-position)))
    (list (save-excursion
            (goto-char b)
            (while (and (< (point) e) (not (eq (following-char) 61)))
              (message "(following-char)=%s" (char-to-string (following-char)))
              (forward-char))
            (cond
             ((eq (point) e)
              b)
             (t
              ;; skip '=' at point
              (goto-char (my-skip-white-space (1+ (point)) 1))
              (point))))
          (cond
           ((eq (char-before e) 59) ; ";"
            (my-skip-white-space (1- e) -1))
           (t
            e)))))

(define-key evil-outer-text-objects-map "t" #'my-evil-a-textobj)
(define-key evil-inner-text-objects-map "t" #'my-evil-inner-textobj)

在hello.js中使用 vat 和 vit 看看效果

定义通用的字符串text object

常用的 "" '' 中选择方法雷同。

用hooks管理大项目的魔法

官方文档:https://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html

可以包含一系列函数。一般是用户自己定义的函数。

范例:

(message "hello world")
(defun after-save-hook-setup ()
  (message "buffer-file-name=%s" buffer-file-name)
  (message "after-save-hook-setup called"))
;;aafafalj
(add-hook 'after-save-hook 'after-save-hook-setup)

M-x save-buffer 可看到对应输出

major-mode-hook

在主模式 major-mode 下用的hook

  • js2
  • …..
(defun js2-mode-hook-setup ()
  (message "js2-mode-hook-setup is called"))
(add-hook 'js2-mode-hook 'js2-mode-hook-setup)

打开一个js文件看看输出

打开多个js文件,配置不同缩进
  • 变量 js-indent-level 缩进默认为2
  • setq-local 只在对应文件本地的变量
(defun js2-mode-hook-setup ()
  (cond
   ((string-match "hello.js$" buffer-file-name)
    (setq-local js-indent-level 4))
   ((string-match "bye.js$" buffer-file-name)
    (setq-local js-indent-level 2))))
(add-hook 'js2-mode-hook 'js2-mode-hook-setup)

用Advice注入病毒至第三方代码中

官方文档:https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html

改变第三方插件行为。

三种注入方式

  • after:函数执行后执行
  • around:可以在函数执行前执行,也可以在函数执行后执行
  • before

卸载注入

(advice-remove 'my-double #'my-increase)

范例 find-file

  • (apply orig-func args) 执行动作。这里是打开文件
  • 变量 kill-ring 存放剪切过的代码
  • kill-new向kill-ring中存放数据
(defun my-find-file-hack (orig-func &rest args)
  (let* ((file (nth 0 args)))
    (when (and file (file-exists-p file))
      (message "file=%s" file)
      (kill-new file))
    (apply orig-func args)))
(advice-add 'find-file :around #'my-find-file-hack)

参考