Emacs
自带的mode
功能也比较强大,而一般初学者(比如我)使用Emacs
时间较短,对
它自身强大的mode
不了解而错失一些可以提高生产力的工具。
winner-mode
是一个全局的minor mode
,它的主要功能是记录窗体的变动。例如当前有
2 个窗口,然后你关了一个,这时可以通过winner-undo
来恢复。还可以再winner-redo
来撤销刚才的undo
.
它默认按键绑定为:
C-c Left winner-undo
C-c Right winner-redo
如果不想它绑定在C-c前缀按键上,可以通过
(setq winner-dont-bind-my-keys nil)
来禁止。
建议配置:
(use-package winner-mode
:ensure nil
:hook (after-init . winner-mode))
同时,它也可以应用在ediff
上,恢复由ediff
导致的窗体变动。
(use-package ediff
:ensure nil
:hook (ediff-quit . winner-undo)
如果平常有注意观察text-scale-adjust
(C-x C-=) 的行为,会发现只需要
按全一次C-x C-=,之后可以只按+
、-
或者0
来缩放字体。而如果触发了
其他按钮则会退出这个状态,它背后主要依赖transient-map
机制。
我们可以仿照着写一个transient-winner-undo
的版本,在需要连续执行winner-undo
的
时候只需要按一个u
就好了。
(defun transient-winner-undo ()
"Transient version of `winner-undo'."
(interactive)
(let ((echo-keystrokes nil))
(winner-undo)
(message "Winner: [u]ndo [r]edo")
(set-transient-map
(let ((map (make-sparse-keymap)))
(define-key map [?u] #'winner-undo)
(define-key map [?r] #'winner-redo)
map)
t)))
着实方便许多!
saveplace
记录了上次打开文件时光标停留在第几行、第几列。如果不想每次打开文件都
要再次跳转到上次编辑的位置,这个mode
可以轻松地应对这种情况。
建议配置:
(use-package saveplace
:ensure nil
:hook (after-init . save-place-mode))
recentf
保存了历史以来访问过的文件,开启之后可以通过recentf-open-files
来打开。
当然如果你使用ivy
全家桶,也可以通过counsel-recentf
访问。
建议配置:
(use-package recentf
:ensure nil
:hook (after-init . recentf-mode)
:custom
(recentf-filename-handlers '(abbreviate-file-name))
(recentf-exclude `("/ssh:"
"/TAGS\\'"
"COMMIT_EDITMSG\\'")))
默认状态下recentf
存储的文件是绝对路径,访问一个在家目录下的文件~/a.cpp
它实际
存储时是用的/home/user/a.cpp
,可以自定义recentf-filename-handlers
来简化存储
的路径。需要注意的是,一旦使用了abbreviate-file-name
,那么recentf-exclude
变
量内的元素也必须要是abbrev
过后的字符串,因为 Emacs 是使用string-prefix-p
来匹
配的。
而COMMIT_EDITMSG
是magit
写commit message
时的文件,记录此文件无意义,故除外。
高亮当前行。
(use-package hl-line
:ensure nil
:hook (after-init . global-hl-line-mode))
如果有订阅 emacs-devel 邮件列表的话会发现曾经有一个投票令 C-x o 调用的是一个 transient-map,后续再切换窗口可以直接用 o
来切换。POLL: make C-x o transient
这样的一个好处是可以减少 Ctrl 按键的使用,降低 RSI 的风险;此外, transient-map 下还有一个 keymap 可以自定义,扩展性就更强了(例如可以在这个 keymap 上再定义 O
为 backward-other-window
函数)。
在最终的实现里则是新增了一个 repeat-mode
, 这个 minor-mode
在 post-command-hook
新增了一个 repeat-post-hook
钩子。这个钩子函数检测如果最后调用的函数有 repeat-map
属性,接下来会对这个 keymap 使用 set-transient-map
。
Emacs 内部已经提供了一些常用的函数 repeat-map:
- 切换窗口
other-window
, 可以使用 C-x o o o 来切换窗口了 - 缩小/增大窗口
enlarge-window
系列,可以使用 C-x { { { 来缩小窗口,其他命令类似 undo
, C-x u u u 来进行 3 次undo
next-error
, M-g n n n 调用 3 次next-error
做一个对比,如果没有启用 repeat-map
, 那么调用 3 次 next-error
需要 M-g n M-g n M-g n,操作极其不连贯。而在 repeat-map
加持下的 M-g n n n 就显得非常自然。
而它的开启方式也非常简单,打开 repeat-mode
这个 minor-mode 即可。
(use-package repeat
:ensure nil
:hook (after-init . repeat-mode)
:custom
(repeat-exit-key (kbd "RET")))
同时 repeated lambda 也支持了,观察其用法发现在 lambda 中设置对应的 repeat-map
即可,由此不难猜出实现。
(defvar other-window-repeat-map
(let ((map (make-sparse-keymap)))
(define-key map "o" 'other-window)
(define-key map "O" (lambda ()
(interactive)
(setq repeat-map 'other-window-repeat-map)
(other-window -1)))
map)
"Keymap to repeat other-window key sequences. Used in `repeat-mode'.")
(put 'other-window 'repeat-map 'other-window-repeat-map)
正因如此,C-x o O o的调用变成可行了。
注意,因为它是在 post-command-hook
里增加了一个钩子,所以会拖慢一个按键的总体运行时间,当然这点差异非常不明显。
如果想查看哪些命令定义了 repeat-map
, M-x describe-repeat-maps 即可。
注: repeat-mode
Emacs 28 上可用
如果你想要一个足够简单的注释与反注释功能,那么自带的newcomment
就可以做到。
(use-package newcomment
:ensure nil
:bind ([remap comment-dwim] . #'comment-or-uncomment)
:config
(defun comment-or-uncomment ()
(interactive)
(if (region-active-p)
(comment-or-uncomment-region (region-beginning) (region-end))
(if (save-excursion
(beginning-of-line)
(looking-at "\\s-*$"))
(call-interactively 'comment-dwim)
(comment-or-uncomment-region (line-beginning-position) (line-end-position)))))
:custom
(comment-auto-fill-only-comments t))
上方的函数它可以完成:
- 当用户选中区间时,在对应区间上注释或者反注释
- 如果当前行是空的,那么会插入一个注释并且将它对齐 (偷懒,直接调用了
comment-dwim
) - 其他情况则对当前行注释或者反注释
这个行为也与evil-nerd-commenter保持一致。
这里有必要比较一下其他comment
函数:
comment-dwim
- 当用户选中区间时,会在对应区间注释或者反注释
- 如果当前行是空的,那么会插入一个注释并且将它对齐
- 如果使用C-u前缀,会则调用
comment-kill
来删除这个注释 - 其他情况下则调用
comment-indent
在尾部插入注释并对齐
comment-line
- 当用户选中区间时,会在对应区间再加上下一行进行注释或者反注释
- 如果当前行是空的,那么只会跳到下一行不会插入注释
- 其他情况下则会将当前行注释或者反注释并跳到下一行
comment-box
看例子就行
(defun add (a b)
(+ a b))
;;;;;;;;;;;;;;;;;;;;;;
;; (defun add (a b) ;;
;; (+ a b)) ;;
;;;;;;;;;;;;;;;;;;;;;;
隐藏、显示结构化数据,如{ }
里的内容。对于单函数较长的情况比较有用。
建议配置:
(use-package hideshow
:ensure nil
:diminish hs-minor-mode
:hook (prog-mode . hs-minor-mode))
hideshow
的默认按键前缀为C-c @,这里放一个默认的按键与经过
evil-mode
的版本的对比表格:
功能 | 原生 | evil-mode |
---|---|---|
hs-hide-block |
C-c @ C-h | zc |
hs-show-block |
C-c @ C-s | zo |
hs-hide-all |
C-c @ C-M-h | zm |
hs-show-all |
C-c @ C-M-s | zr |
hs-hide-level |
C-c @ C-l | 无 |
hs-toggle-hiding |
C-c @ C-c | za |
一些类似hideshow
的插件
其中origami
有lsp
支持lsp-origami
默认情况下hideshow
对于显示的代码是以...
overlay
的形式显示的,而且
hideshow
给予了自定义的能力,通过设置hs-set-up-overlay
变量即可。
;; 这里额外启用了 :box t 属性使得提示更加明显
(defconst hideshow-folded-face '((t (:inherit 'font-lock-comment-face :box t))))
(defun hideshow-folded-overlay-fn (ov)
(when (eq 'code (overlay-get ov 'hs))
(let* ((nlines (count-lines (overlay-start ov) (overlay-end ov)))
(info (format " ... #%d " nlines)))
(overlay-put ov 'display (propertize info 'face hideshow-folded-face)))))
(setq hs-set-up-overlay 'hideshow-folded-overlay-fn)
附效果图:
显示空白字符,如\t
\f
\v
空格等等。
可以配置在prog-mode
,markdown-mode
和conf-mode
下,显示行尾的空白字符。
(use-package whitespace
:ensure nil
:hook ((prog-mode markdown-mode conf-mode) . whitespace-mode)
:config
(setq whitespace-style '(face trailing)))
当然,仅显示行尾空白字符也可以简单地设置show-trailing-whitespace
为t
来开启。
kinono 分享的配置:
(use-package whitespace
:ensure nil
:hook (after-init . global-whitespace-mode) ;; 注意,这里是全局打开
:config
;; Don't use different background for tabs.
(face-spec-set 'whitespace-tab
'((t :background unspecified)))
;; Only use background and underline for long lines, so we can still have
;; syntax highlight.
;; For some reason use face-defface-spec as spec-type doesn't work. My guess
;; is it's due to the variables with the same name as the faces in
;; whitespace.el. Anyway, we have to manually set some attribute to
;; unspecified here.
(face-spec-set 'whitespace-line
'((((background light))
:background "#d8d8d8" :foreground unspecified
:underline t :weight unspecified)
(t
:background "#404040" :foreground unspecified
:underline t :weight unspecified)))
;; Use softer visual cue for space before tabs.
(face-spec-set 'whitespace-space-before-tab
'((((background light))
:background "#d8d8d8" :foreground "#de4da1")
(t
:inherit warning
:background "#404040" :foreground "#ee6aa7")))
(setq
whitespace-line-column nil
whitespace-style
'(face ; visualize things below:
empty ; empty lines at beginning/end of buffer
lines-tail ; lines go beyond `fill-column'
space-before-tab ; spaces before tab
trailing ; trailing blanks
tabs ; tabs (show by face)
tab-mark ; tabs (show by symbol)
)))
比较好的是能指示过长的行,这样都不需要装那种显示一条竖线的插件了。
有时候会打开一些文件,这些文件里的某一行特别长,而Emacs
没有针对这种情况做特殊
处理,会导致整个界面卡死。现在它来了!
直接全局启用:
(use-package so-long
:ensure nil
:config (global-so-long-mode 1))
当打开一个具有长行的文件时,它会自动检测并将一些可能导致严重性能的mode
关闭,
如font-lock
(syntax highlight
)。
注:Emacs
27+ 自带
当遇到驼峰式的变量时,如CamelCasesName
,但是你比较喜欢GNU
式的命名方式(使用
下划线),那么你可以开启glasses-mode
。它只会让CamelCasesName
显示成
Camel_Cases_Name
而不会对原文件做出修改。
不过,大写字母加下划线的组合有点奇怪。
由kinono分享。
subword
可以处理CamelCasesName
这种驼峰式的单词,M-f
(forward-word
) 后,光标会依次停在大写的词上。
(use-package subword
:ensure nil
:hook (after-init . global-subword-mode))
如果不想全局打开,也可以只利用subword-forward
等移动命令。
此外,subword
包还提供了一个模式叫做superword-mode
。在这个模式下,
this_is_a_symbol
被认为是一个单词。 M-f (forward-word
) 可以直接跳
过。
如果你的屏幕很宽,但是实际显示的条目的宽度无法利用这宽屏幕,那么follow-mode
可
以帮助你。一个典型的使用案例是,再打开一个窗口,然后对当前buffer
开启
follow-mode
,这样之后另一个窗口显示的内容会是当前窗口的后续。例如,一个文件有
100行,当前buffer
只能显示10行,那么另一个窗口将会显示下面10行。如果嫌窗口数还
是太少,可以继续增多。
由Kermit95分享。
选中文本后,直接输入就可以,省去了删除操作。这在其他文本编辑器里都是标配,建议打开。
(use-package delsel
:ensure nil
:hook (after-init . delete-selection-mode))
高亮显示配对的( )
[ ]
{ }
括号,比较实用,建议打开。
(use-package paren
:ensure nil
:hook (after-init . show-paren-mode)
:config
(setq show-paren-when-point-inside-paren t
show-paren-when-point-in-periphery t))
simple
包提供的基础命令非常多,这里会慢慢完善。
(use-package simple
:ensure nil
:hook (after-init . (lambda ()
(line-number-mode)
(column-number-mode)
(size-indication-mode))))
在注释中看到不认识的单词,一般做法是复制、打开终端、然后调用外部程序
ydcv
来翻译。
最近发现了shell-command-on-region
之后,可以省掉前2个步骤了!
例如papaya
单词不认识,那么选中它,然后M-|
(shell-command-on-region
)运行一下,输入ydcv
然后执行。如果输出内容比较少,则
直接会在echo area
处显示(当resize-mini-windows
不为nil
时由
max-mini-window-height
决定)。如果想强制它输出在buffer
下(方便复制),可以在
上面再封装一层、利用advice
机制,或者提前创建一个名叫*Shell Command Output*
的
buffer
。
以下是一个advice
的例子(不推荐使用advice
,有较大的侵入性,这里只是做个演示)
(define-advice shell-command-on-region (:around (func &rest args))
(let ((max-mini-window-height 0))
(apply func args)))
甚至可以直接封装一个叫做ydcv
的命令来完成这个工作!
(defun ydcv (beg end)
(interactive "r")
(let ((max-mini-window-height 0))
(shell-command-on-region beg end "ydcv")))
当然还可以有ydcv-at-point
, ydcv-dwim
等一系列函数!
有时候Emacs
里打开的文件可能被外部修改,启用autorevert
的话可以自动更新对应的
buffer
.
(use-package autorevert
:ensure nil
:hook (after-init . global-auto-revert-mode))
本身Emacs
自带的isearch
已经足够强大,稍加修改就可以增加实用性。
例如anzu
的显示匹配个数的功能就已经原
生支持了。通过
(setq isearch-lazy-count t
lazy-count-prefix-format "%s/%s ")
来显示如 10/100
这种状态。
比较恼人的一点是,在搜索中删除字符会回退搜索结果,而不是停在当前位置将最后一个搜
索字符删除。这里可以通过remap isearch-delete-char
来实现。
此外,还可以将搜索结果保持在高亮状态以方便肉眼识别。这个是通过设置
lazy-highlight-cleanup
为nil
实现的。去除高亮状态需要人工M-x
调用
lazy-highlight-cleanup
。
(use-package isearch
:ensure nil
:bind (:map isearch-mode-map
([remap isearch-delete-char] . isearch-del-char))
:custom
(isearch-lazy-count t)
(lazy-count-prefix-format "%s/%s ")
(lazy-highlight-cleanup nil))
注:isearch-lazy-count
和lazy-count-prefix-format
需要Emacs
27+
在浏览器里,我们只需要按C-f,然后敲入所要搜索的字符串。之后只要按回车 就可以不断地向下搜索。如果我们需要向上搜索,那么需要点击一下向上的箭头。
现在我们在isearch
里模拟这种情况,还是使用C-s来调用isearch
。但是之
后的repeat
操作是交给了回车。
首先,我们先定义一下变量来保存当前搜索的方向。
(defvar my/isearch--direction nil)
然后使得isearch-mode-map
下的C-s可以告诉我们当前是在向下搜索;同理,
使得isearch-mode-map
下的C-r告诉我们是在向上搜索。
(define-advice isearch-repeat-forward (:after (_))
(setq-local my/isearch--direction 'forward))
(define-advice isearch-repeat-backward (:after (_))
(setq-local my/isearch--direction 'backward))
这里偷懒,采用了advise
的方式。如果不想侵入,可以自己在上层包装一下对应的命令。
然后在isearch-mode-map
下的回车操作就是根据my/isearch--direction
来搜索了。就
是如此简单。
(defun my/isearch-repeat (&optional arg)
(interactive "P")
(isearch-repeat my/isearch--direction arg))
当然在按Esc
键的时候表明搜索已经结束了,此时应该重置当前的方式:
(define-advice isearch-exit (:after nil)
(setq-local my/isearch--direction nil))
完整代码见下方:
(use-package isearch
:ensure nil
:bind (:map isearch-mode-map
([return] . my/isearch-repeat)
([escape] . isearch-exit))
:config
(defvar my/isearch--direction nil)
(define-advice isearch-exit (:after nil)
(setq-local my/isearch--direction nil))
(define-advice isearch-repeat-forward (:after (_))
(setq-local my/isearch--direction 'forward))
(define-advice isearch-repeat-backward (:after (_))
(setq-local my/isearch--direction 'backward))
)
tempo
可以算是yasnippet
的祖先,
skeleton
算是它的爷爷。由于tempo
里可以使用elisp
函数,灵活性非常大。
实际上在写代码的时候,想插入一个LICENSE头是个比较常用的需求,它也可以通过其
他方式如auto-insert
在打开文件时就自动插入。在这里,我们使用tempo
来实现。
目前比较推荐的方式是采用SPDX
的格式,而不是直接把license
内容写入代码文件中。
采用SPDX
格式可以有效的减少文件大小,不会喧宾夺主占用大量代码行数。
一个典型的license
头是这样:
// Copyright 2017 - 2018 ccls Authors
// SPDX-License-Identifier: Apache-2.0
所以我们可以仿照着这个格式来写一个tempo
的template
.
;; 完整的列表非常长,可以访问 https://spdx.org/licenses/ 获得
(defconst license-spdx-identifiers
'(Apache-1.0 Apache-2.0 MIT))
(tempo-define-template "license"
'(comment-start
(format "Copyright %s - present %s Authors"
(format-time-string "%Y")
(if (featurep 'projectile)
(progn
(require 'projectile)
(projectile-project-name))
"Unknown"))
comment-end > n>
comment-start
"SPDX-License-Identifier: " (completing-read "License: "
license-spdx-identifiers)
comment-end > n>)
"license"
"Insert a SPDX license.")
tempo
内的>
表示的是缩进,n
表示的是插入一个换行,其他的部分就是一个普通的
elisp
函数了。
这样定义了这个template
之类,会生成一个叫tempo-template-license
的函数。因此我
们可以直接调用它来插入license
头部。
此外还可以结合abbrev-mode
来自动替换,如果想在elisp-mode
下直接替代,可以通过
define-abbrev
来实现:
(define-abbrev emacs-lisp-mode-abbrev-table "2license" "" 'tempo-template-license)
这里只需要在elisp-mode
下开启abbrev-mode
,然后输入2license
就会实现自动替换
(注意,最后要有一个空格)。
此外,abbrev-expand
是以 WORD 为展开,如果在emacs-lisp-mode
下使用;license
则
会不生效,因为;license
是由 2 个单词组成的。
PS: 可以通过C-h s (describe-syntax
) 来查看当前当前 mode 的 syntax-table.
如果实在想通过多个 WORD 来转换也不是不行,注意到define-abbrev-table
里是使用
:regexp
来指定所提取的内容。于是可以使用\w \w
来匹配前面的 2 个单词,代码如下:
;; From https://lists.gnu.org/archive/html/emacs-devel/2021-01/msg00553.html
(define-abbrev-table 'fundamental-mode-abbrev-table
'(
("a que" "a qué" nil :count 0))
"Uwe's table"
:regexp "\\(\\w+ \\w+\\)")
可以切换到fundamental-mode
体验一下。
听说有些写java
的朋友特别喜欢将变量的=
对齐,即原来的代码是这样的:
private int magicNumber = 0xdeadbeef;
private double PI = 3.14159265358939723846264;
选中它们,然后调用align-regexp
,给定=
作为它的参数,就会将上述代码的=
部分对
齐了。
private int magicNumber = 0xdeadbeef;
private double PI = 3.14159265358939723846264;
其他align
相关的函数功能还有待开发。
dired
是一个用于directory
浏览的mode
,功能非常丰富。因此这里介绍的东西肯定不
能完全覆盖,会慢慢完善之。
在dired-mode-map
中,也是可以执行shell
命令的。与之相关的命令有
dired-do-shell-command
, 默认绑定在!dired-smart-shell-command
,默认绑定在M-!async-shell-command
,默认绑定在M-&
其中,通过配置dired-guess-shell-alist-user
可以令dired-do-shell-command
有一个
比较好的默认命令。例如,我这是样配置的:
(setq dired-guess-shell-alist-user `((,(rx "."
(or
;; Videos
"mp4" "avi" "mkv" "flv" "ogv" "mov"
;; Music
"wav" "mp3" "flac"
;; Images
"jpg" "jpeg" "png" "gif" "xpm" "svg" "bmp"
;; Docs
"pdf" "md" "djvu" "ps" "eps")
string-end)
,(cond ((eq system-type 'gnu/linux) "xdg-open")
((eq system-type 'darwin) "open")
((eq system-type 'windows-nt) "start")
(t "")))))
这里考虑了多个平台下的差异。如linux
平台下会使用xdg-open
来打开对应的这些文件
(通过mimeinfo
来配置,见~/.config/mimeapps.list
)。但是它有一个缺点,会阻塞当
前的Emacs
进程,所以仅适用于临时查看的需求。
dired-smart-shell-command
与dired-do-shell-command
类似,也会阻塞当前Emacs
进
程。
async-shell-command
则不会阻塞当前Emacs
,唯一的缺点可能是会多弹出个buffer
吧。
如果对async-shell-command
的结果不是很感兴趣,可以通过shackle
等类似的工具忽略
对应的buffer
。
如果使用的是Emacs
28的话,并且已经设置了
(setq browse-url-handlers '(("\\`file:" . browse-url-default-browser)))
可以直接在dired
里按W
(browse-url-of-dired-file
), 这会直接用外部程序打开。
当然,它不会阻塞Emacs
。
dired
显示文件时使用的ls
命令参数是由dired-listing-switches
来控制的,它的默
认值是-al
。如果不想要显示以.
开头的文件,那么通过C-u s (s
为
dired-sort-toggle-or-edit
)来重新设置dired-listing-switches
。
如果只是想简单地隐藏当前目录下以.
开头的文件,那么可以通过将满足^\\.
正则的行
删除就行(真实文件并没有删除,只是删除它的显示)。注意到dired-do-print
命令基本
不怎么使用,于是可以利用advice
来覆盖它,实现我们自己的dotfiles-toggle
。
;; 修改自 https://www.emacswiki.org/emacs/DiredOmitMode
(define-advice dired-do-print (:override (&optional _))
"Show/hide dotfiles."
(interactive)
(if (or (not (boundp 'dired-dotfiles-show-p)) dired-dotfiles-show-p)
(progn
(setq-local dired-dotfiles-show-p nil)
(dired-mark-files-regexp "^\\.")
(dired-do-kill-lines))
(revert-buffer)
(setq-local dired-dotfiles-show-p t)))
这样只要按一下P就可以达到隐藏、显示的切换了。
如果不想自己写elisp
,这里也有一个现成的包 https://github.com/mattiasb/dired-hide-dotfiles
之前要完成这个功能,需要将 dired-mode 中 RET 绑定的 dired-find-file
remap 至 dired-find-alternate-file
。但是 Emacs 28 现在直接提供了一个用户选项来开启这个功能。
(use-package dired
:ensure nil
:custom
(dired-kill-when-opening-new-dired-buffer t))
即可。
;; For Emacs 27
(use-package dired
:ensure nil
:bind (:map dired-mode-map
([remap dired-find-file] . dired-find-alternate-file))
:config
;; Enable the disabled dired commands
(put 'dired-find-alternate-file 'disabled nil))
默认 dired-find-alternate-file
是被禁用的,需要将其解禁。
注意: dired-up-directory
还是会创建新的 buffer.
ispell
全称是interactive
spell
检查器,它支持ispell
, aspell
和hunspell
,
以下以hunspell
为例。
;; 这里使用的是 en_US 字典,需要使用包管理安装对应的字典,类似的名字可能 hunspell-en_US
(setq ispell-dictionary "en_US"
ispell-program-name "hunspell"
ispell-personal-dictionary (expand-file-name "hunspell_dict.txt" user-emacs-directory))
这样就可以通过调用ispell-word
来看一个单词是否正确了。如果是evil
用户,这个函
数已经被绑定至z=上了。 \w/
Emacs
里日历可以拿来干什么呢?
第一个作用自然是看日期的,最起码得让今天醒目得吧?于是选择了在
calendar-today-visible-hook
上加上calendar-mark-today
。默认今天的日期是有下划
线的,如果不喜欢也可以自己修改calendar-today-marker
。
第二个作用自然是看节日的,为了更加更本地化一点,可以设置一些自己想关注的节日。我 是这样设置的:
把较本土的节日放在了holiday-local-holidays
里,
;; 分别是妇女节、植树节、劳动节、青年节、儿童节、教师节、国庆节、程序员节、双11
(setq holiday-local-holidays `((holiday-fixed 3 8 "Women's Day")
(holiday-fixed 3 12 "Arbor Day")
,@(cl-loop for i from 1 to 3
collect `(holiday-fixed 5 ,i "International Workers' Day"))
(holiday-fixed 5 4 "Chinese Youth Day")
(holiday-fixed 6 1 "Children's Day")
(holiday-fixed 9 10 "Teachers' Day")
,@(cl-loop for i from 1 to 7
collect `(holiday-fixed 10 ,i "National Day"))
(holiday-fixed 10 24 "Programmers' Day")
(holiday-fixed 11 11 "Singles' Day")))
再把其他没在默认日历里的放进holiday-other-holidays
里,
;; 分别是世界地球日、世界读书日、俄罗斯的那个程序员节
(setq holiday-other-holidays '((holiday-fixed 4 22 "Earth Day")
(holiday-fixed 4 23 "World Book Day")
(holiday-sexp '(if (or (zerop (% year 400))
(and (% year 100) (zerop (% year 4))))
(list 9 12 year)
(list 9 13 year))
"World Programmers' Day")))
然后再开启calendar
内置的中国节日支持:
(setq calendar-chinese-all-holidays-flag t)
这样就可以获得一个不错的日历体验了。如果自己还有农历节日需求的话,可以使用
holiday-chinese
来定义。如
;; 元宵节
(setq holiday-oriental-holidays '((holiday-chinese 1 15 "Lantern Festival")))
当然元宵节已经默认被定义了,只需开启calendar-chinese-all-holidays-flag
。
如果这还不够,还有cal-china-x。
第三个功能也可以在calendar
界面添加日记,默认的日记从功能上来说自然是不如
org-mode
加持的丰富。请确保org-agenda-diary-file
的值不是'diary-file
,然后在
calendar-mode-map
下调用org-agenda-diary-entry
即可选择插入日记。
附图:
需要注意,它默认不会自动保存org-agenda-diary-file
。如果不喜欢这一点,可以利用
advice
来修正一下。
(defun org-agenda-add-entry-with-save (_type text &optional _d1 _d2)
;; `org-agenda-add-entry-to-org-agenda-diary-file'里认为如果用户没有输入有效的
;; 内容,会弹出对应 buffer 让用户人工输入。
(when (string-match "\\S-" text)
(with-current-buffer (find-file-noselect org-agenda-diary-file)
(save-buffer))))
(advice-add #'org-agenda-add-entry-to-org-agenda-diary-file :after #'org-agenda-add-entry-with-save)
我觉得这样子设置之后,可以轻度取代 org-journal了?
如果你想用鼠标来控制Emacs
的行为,有点像现在浏览器上的鼠标手势。不过它只能识别
鼠标移动轨迹所描绘的形状,不能判断它的方向。
- 执行
strokes-mode
打开minor-mode
- 执行
strokes-global-set-stroke
在弹出的buffer
内使用Shift
+鼠标左键(也可以 用中键)绘出想作为快捷操作的大致形状,假设是一个 C 的形状,然后鼠标右键结束绘 制。稍后会提示输入与stroke
对应的命令,假设是strokes-help
- 移动鼠标,使得它的轨迹是个 C 的形状
Shift
+鼠标中键以执行与这个stroke
对应的命令,也就是strokes-help
想要更详细的信息?请M-x strokes-help
.
你想在Emacs
里快速调用搜索引擎搜索吗?原来这个功能早已经内置了!
由于webjump-sites
早已有默认值了,如果想急着体验一下可以立即M-x webjump
。其原
理也是相当简单,通过用户选择它想要用的搜索引擎+查询内容构造出实际url
,然后通过
browse-url
调用托管给浏览器。
我的配置是这样的:
(use-package webjump
:ensure nil
:bind ("C-c /" . webjump)
:custom
(webjump-sites '(
;; Emacs.
("Emacs Home Page" .
"www.gnu.org/software/emacs/emacs.html")
("Savannah Emacs page" .
"savannah.gnu.org/projects/emacs")
;; Internet search engines.
("DuckDuckGo" .
[simple-query "duckduckgo.com"
"duckduckgo.com/?q=" ""])
("Google" .
[simple-query "www.google.com"
"www.google.com/search?q=" ""])
("Google Groups" .
[simple-query "groups.google.com"
"groups.google.com/groups?q=" ""])
("Wikipedia" .
[simple-query "wikipedia.org" "wikipedia.org/wiki/" ""]))))
如果只是想要简单的查询,那么可以作为 engine-mode的内置替换方案了。
当然,还可以配置多级查询选项,可参考webjump-to-iwin
的实现。
因为transient-map
的优先级比其他keymap
都要高,所以可以将它当作菜单来使用。
如果嫌set-transient-map
用起来不方便,可以使用hydra代替。
如果在编辑文字时发现要拷贝一个url
,但是当前窗口内有多个url
,类似的场景如下:
/// url1: https://www.google.com
/// url2: https://www.baidu.com
/// url3: https://duckduckgo.com
int foo(int x) {
const char* url = "";
}
想要为url
赋值为注释内的 3 个 url 之一。
- 首先将光标移动到想复制的
url
处 - 再将这个
url
复制到kill-ring
当中 - 再回到原来的位置
- 再粘贴
(defhydra hydra-copy (:color blue)
"Copy"
("w" copy-word-at-point "word")
("u" copy-url-at-point "url")
("q" nil "cancel"))
(defun copy-url-at-point ()
"Copy url at point."
(interactive)
(save-excursion
(avy-goto-word-or-subword-1)
(kill-new (thing-at-point 'url))))
这里使用hydra
来偷懒一下。
效果图:
历史老物,1994 年的时候就已经出现了。
打字打累了,想休息一下?看代码看累了,想放松一下?
那么它可能会适合你。如果在一段时间内的敲击键盘次数大于阈值,那么它会假设平均速度35 wpm
,每个单词长度5来推算出要休息多少分钟。
而到达休息状态时,它可能会显示出一个汉诺塔移动的动画。可以M-x type-break
立即体验!
这是一个计算时间到底去哪里了的包,不过都有org-mode
了,真的还会有人来用这个吗?
org | timeclock |
---|---|
org-clock-in |
timeclock-in |
org-clock-out |
timeclock-out |
功能与org-mode
几乎一致,不过它可以随时timeclock-out
不用管记录时间的文件打开与否,而在org-mode
中clock-out
则要保证运行clock
的那个文件还处于打开状态。
依旧是怀旧向的内置包,可以将源代码文件头部中大量的license
说明折叠起来,效果跟hideshow
包类似。可以通过配置elide-head-headers-to-hide
来自定义想要的折叠区间。最近在 Emacs 29 中引入了 elide-head-mode
minor mode, 这样就不必再手工调用 elide-head
和 elide-head-show
了。这两个函数已经被标记为 obsoleted 了。
在晚上零点的时候定期执行一些任务,默认是clean-buffer-list
,可以设置midnight-hook
来自定义行为。
M-x midnight-mode 来开启深夜模式。嗯,又到了深夜网抑云音乐时间了。
Emacs 下有几个类似终端模拟器(其实有些不算是),内置的有这 3 个: shell-mode
, term-mode
, eshell
。
如果你不喜欢用 M-x compile 来编译,习惯在 shell-mode
, term-mode
, eshell
下直接使用 gcc 或者 make 来编译, 那么你可能需要compilation-shell-minor-mode
。它可以识别报错,令错误可以点击,快速打开报错文件。自然在调用 M-x compile 的就是用的 compilation-mode
了。
term-mode
算是一个完整的终端模拟器,与外部的终端模拟器相比除了刷新速度慢、色彩显示较差之外就没有其他差别了。因此如果是在 Linux/MacOS 平台下且只在本地使用,是比较推荐 term-mode
的。term-mode
分别可以通过 term
和 ansi-term
命令启动,唯一的区别是由 term
命令启用的终端模拟器下面 C-x 是直接被终端给捕获了,想要在这个模式下使用 C-x C-f 来打开文件还需要再额外地做设置,而且重复地使用 term
命令只会打开一个 buffer。如果你想多次调用分别打开多个 buffer 的话推荐使用 ansi-term
命令。对我个人而言我是更喜欢 ansi-term
命令。
;; 可以使用这种方式将需要的按键解绑
(use-package term
:ensure nil
:bind (:map term-raw-map
("M-:" . nil)
("M-x" . nil)))
当然在 term-mode
里使用 htop
, git
, fzf
, neofetch
这种类似工具是没啥大问题的,但是使用 vim
的话就有点拉胯了。一是显示效果非常差,代码高亮都无法显示;二是也不推荐在 Emacs 里使用 vim
, 编辑文件直接 C-x C-f 就好。
这里不得不提一下, term-mode
里两种模式,一个是 char-mode
, 另一个是 line-mode
。 在 char-mode
下输入任意一个字符都会直接转发至当前的进程,而 line-mode
下则只会遇到 \n
的时候才会将以前的内容一起转发。就拿 htop
这个命令来说,在 char-mode
下按一下 q
会直接退出,按一下 C-n
会移动光标,但是一旦切换到 line-mode
下后就完全变了,连续地按 q
不会退出,直到你按下 Enter 键。
term-mode
还有一个非常大的优点是与 Emacs 生态的结合。其中一个是可以快速地跳转到上一次的 prompt 处(一般的终端模拟器都没有这个功能),想要启用这功能需要配置 term-prompt-regexp
变量,而它的默认值非常不友好,竟然是一个 ^
表示跳转到开头。建议修改成如下配置,毕竟我们要在 term-mode
里使用 shell, 这个配置也是它的注释里所推荐使用的:
(setq term-prompt-regexp "^[^#$%>\n]*[#$%>] *")
这样就可以使用 C-c C-p 和 C-c C-n 来上下跳转 prompt 了。
另外一点是目录同步,如果你在 term-mode
下进入了 /tmp
目录,那么在 Emacs 按 C-x C-f 就会尝试打开此目录下的文件。如果你是 bash 用户那么这个甚至不需要你配置,其他 shell 用户就必须要在对应 shell 的配置里增加如下配置:
# 这是 zsh 需要做的修改
#
# INSIDE_EMACS 则是 Emacs 在创建 term/shell/eshell 时都会带上的环境变量
# 通常 shell/tramp 会将 TERM 环境变量设置成 dumb,所以这里要将他们排除。
#
# shell 下的目录同步不采用这种方式
function precmd() {
if [[ -n "$INSIDE_EMACS" && "$TERM" != "dumb" ]]; then
echo -e "\033AnSiTc" "$(pwd)"
echo -e "\033AnSiTh" $(hostname -f)
echo -e "\033AnSiTu" "$LOGNAME"
fi
}
更详细的说明可以见 AnsiTermHints。
其实它就是在每条命令执行前将自己当前的目录告诉了 term-mode
, 然后 term-mode
再设置 default-directory
变量。
另外一种方式则是依赖 Linux 的 procfs
, 可以获得 term-mode
启动的 shell 进程 pid,然后通过读 /proc/pid/cwd/
来获取当前路径。
(defun term-directory-sync ()
"Synchronize current working directory."
(interactive)
(when term-process
(let* ((pid (process-id term-process))
(dir (file-truename (format "/proc/%d/cwd/" pid))))
(setq default-directory dir))))
;; term-process 则是在 term-mode-hook 中通过
;;
;; (get-buffer-process (current-buffer))
;;
;; 获得
;; 注:以上这种方式对于 vterm 同样适用,因为 vterm 直接暴露了 vterm--process 使用
;; 起用更加方便
如果你嫌 term-mode
的刷新速度太慢、颜色显示太差,可以使用 vterm
, 但是它的目录同步方式完全与 term-mode
不同,这点需要注意。
Emacs 自带一个 term-paste
函数,可以在 char mode 里粘贴文本。不过有时候我们复制的文本最后有个换行,一粘贴就会立即运行,很讨厌。下面这个版本处理了有换行符和多行的情况:
(defun my-term-yank ()
"Paste recent kill into terminal, in char mode."
(interactive)
(when-let ((text (current-kill 0))
;; Remove newlines at the beginning/end.))
(text (string-trim text "\n+" "\n+")))
(when (or (not (string-match-p "\n" text))
(y-or-n-p "You are pasting a multiline string. Continue? "))
(term-send-raw-string text))))
可以把它绑定到 term-raw-map
的 C-M-v
上,用起来就和专门的终端模拟器差不多了。
下面的代码提供了 my-term-browse-mode
命令,执行以后就会把终端变成一个只读的普通 buffer,快捷键也和普通 buffer 一样,方便我们浏览比较长的输出或者复制东西。
(defvar my-term-browse-mode-map
(make-sparse-keymap)
"Keymap for `my-term-browse-mode'.")
(defun my-term-browse-mode ()
"Turn the terminal buffer into a read-only normal buffer."
(interactive)
;; Workaround: Without this code, there's a bug: Press `C-p' in char mode to
;; browse history, then `C-n' to go back, then `my-term-browse-mode', then
;; `C-n', you'll find a newline is produced. Call `term-char-mode', that
;; newline is sent to the shell. This is not a problem with
;; `my-term-browse-mode', since `term-line-mode' also has it.
(let ((inhibit-read-only t))
(save-excursion
(goto-char (point-max))
(while (eq (char-before) ?\n)
(delete-char -1))))
;; Idea: We could put a `read-only' property to the region before
;; `process-mark', so current input could be edited, but I think there's
;; little benefit.
(setq buffer-read-only t)
(remove-hook 'pre-command-hook #'term-set-goto-process-mark t)
(remove-hook 'post-command-hook #'term-goto-process-mark-maybe t)
(use-local-map my-term-browse-mode-map))
browse mode 下的键位由 my-term-browse-mode-map
指定,可以把 term-char-mode
, term-previous-prompt
, term-next-prompt
等命令绑定在里面。
PS: 如果只是从上到下浏览的话可以 C-c C-q 开启 term-pager 模式,然后运行 cat /tmp/long-lines.txt
的效果就会像 more /tmp/long-lines.txt
一样。
shell-mode
它实际上不算是一个终端模拟器,它只是简单包装了一下 shell, 所以只能执行一些简单的命令, htop
这种存在复杂交互的应用就不行了。它也支持上下跳转到 prompt 处,而且它的默认值足够通用,如果不适用的话用户再自己配置一下 shell-prompt-pattern
. 通过 C-c C-p 和 C-c C-n 来上下跳转 prompt.
自然 shell-mode
也支持目录同步,不过它的同步方式与 term-mode
不同。 term-mode
是要求 shell 主动告诉 Emacs,而 shell-mode
是启用了 shell-dirtrack-mode
使用正则匹配如 cd
, pushd
等各种可能改变当前目录的各种命令来达到的。
如果发现在 shell-mode
下目录显示不正确时,可以通过 M-x shell-resync-dirs 来同步。
与 term-mode
相比而言它实在是没啥多大优势,但是如果你是在通过 tramp
编辑一个远程的文件,想在远程机器上运行一些命令,可以直接 M-x shell 登录远端的机器,而 term-mode
则不会识别这种情况,仍是创建一个本地的终端环境。在有 tramp
的情况下, shell-mode
下路径显示在 cd
改变了当前工作目录之后会显示出错, PR 的机会又来了!
在 shell-mode
里没法像终端模拟器那样通过 M-. 来直接输入上一命令的最后一个参数,但是多数 shell 都实现提供了一个内部变量 $_
支持。
echo hello
echo $_
# 输出如下
#
# hello
# hello
另外一个独到的地方是它可以当做 sh 文件的 REPL。例如你在编写这样的一个 sh 脚本:
echo 'hello, world'
可以直接输入 C-c C-n (sh-send-line-or-region-and-step
) 将当前行发送至 shell 执行。
eshell
则是完全由 elisp 实现的 shell,正因为它是 elisp 实现的,所以在所有地方下都可以使用(推荐 Windows 用户使用)。当然也因为它是 elisp 实现的,所以速度上会稍微慢一点。此外如果你是在远程编辑文件,那么使用 eshell
可以直接编辑远程文件,因为它是完全用 elisp 实现的,可以共享当前 Emacs 的状态,自然 tramp 也是可以直接共享的,当然目录同步的更不在话下了。
它的语法与与 bash/zsh 的语法不完全一致,例如一个 for 循环
# 在 bash/zsh 里这样写的
for i in *.el; do
rm $i
done
# 在 eshell 里则是这样写的
for i in *.el {
rm $i
}
在 eshell
里执行类似 htop
, git diff
这样的命令也是可以的,但是它不是直接支持,而是间接调用 term-mode
来完成。只需要将对应的命令加入至 eshell-visual-commands
, eshell-visual-subcommands
和 eshell-visual-options
中即可,建议配置:
(use-package em-term
:ensure nil
:custom
(eshell-visual-commands '("top" "htop" "less" "more" "bat"))
(eshell-visual-subcommands '(("git" "help" "lg" "log" "diff" "show")))
(eshell-visual-options '(("git" "--help" "--paginate"))))
因为 eshell
不能复用其他 shell 的插件,所以 eshell
有自己的生态,可以考虑使用 eshell-git-prompt
等包。
eshell
还有一个最大的缺点是补全系统。其他 shell 都有自己的 bash-completion
, zsh-completion
包,但是 eshell 却没有,但是它只提供了一个基础的补全功能模块 pcomplete
. 通过它我们也可以完成基础命令的补全,但是如果想要全部实现的话还是得费一番功夫的。基于这个痛点,Emacs 社区有相应的增强包 emacs-fish-completion, 在补全时将对应的命令发送给 fish 然后再截获、解析它的输出。以这种形式扩展的 pcomplete 不用再重复走 bash-completion
, zsh-completion
的路。
因为 eshell
与 shell-mode
都使用了 pcomplete, 所以这两者都能够享受到由此带来的补全效果。
默认情况下,在 eshell
里 C-d 只会删除字符不会在当前输入为空时退出、 M-. 不会自动插入上一命令的最后一个参数,这可能会令习惯使用外部 shell 的用户非常不习惯,可以通过如下配置将它们带回来。
;; eshell 自己有实现的一个比较好的 C-d 函数,但是它默认没有开启
;; 这里显式地将这个函数导出。
(use-package em-rebind
:ensure nil
:commands eshell-delchar-or-maybe-eof)
;; Emacs 28 可以直接定义在 eshell-mode-map 里,但是 27 的话需要将相关的键绑定定义在
;; eshell-first-time-mode-hook 这个 hook 里
(use-package esh-mode
:ensure nil
:bind (:map eshell-mode-map
("C-d" . eshell-delchar-or-maybe-eof)
("M-." . eshell-yank-last-arg))
:config
(defun eshell-yank-last-arg ()
"Insert the last arg of the previous command."
(interactive)
(insert "$_")
(pcomplete-expand))
)
关于 $_
的说明可以看 eshell 文档的 Expansion 节。 eshell 这样设计也与其他 shell 保持一致。
如果在 C-x C-f 时不小心误按 C-TAB (默认是 file-cache-minibuffer-complete
,不过好像 helm 把这个绑定给去掉了) 就会发现,其实 Emacs 还自带了一个叫做 file cache 的东西。
假设当前目录结构是这样的:
$ tree
.
├── proj1
│ ├── main.cpp
│ └── Makefile
└── proj2
├── main.cpp
└── Makefile
2 directories, 4 files
在使用 M-x file-cache-add-directory-recursively
(如果嫌太慢可以用 file-cache-add-directory-using-find
, 实际是调用的 find . -name '*'
) 将这个目录以及子目录下的文件添加到 file cache 中。
然后在 C-x C-f 的时候,即使当前目录是在 /usr/share/include/
,输入 main
然后再按 C-TAB 仍然可以补全成 /path/to/proj1/main.cpp
. 因为 file cache 里存在多个候选项,再次按 C-TAB 就会切换成 /path/to/proj2/main.cpp
.
如果想把 file cache 给清除掉,那么就直接 M-x file-cache-clear-cache
即可。另还可以使用 M-x file-cache-display
来查看 file cache 的内容。
注意,file cache 本身没有做数据的持久化,重启 Emacs 会丢失 file cache,因此如果想每次都让 Emacs 加载一些文件的话得在配置里人工指定。实际上 file cache 存储在 file-cache-alist
变量中,因此可以自己保存此变量。
;; 将 ~/projects 下的所有文件都加入
(file-cache-add-directory-using-find "~/projects")
;; 通常是 ~/.emacs.d/elpa/ 包下文件
(file-cache-add-directory-list load-path)
单项目下的文件跳转更适合用 projectile
/project
,如果频繁在多个项目间跳转,那么可以尝试将多个项目的文件都加入到 file cache 中。
如果你频繁地在一些 buffers 中操作,那么可以把这些 buffers 加入到 filesets 组中。Emacs manual 中推荐的用法需要用户手工调用 filesets-init
,但是它会额外地构建 menu-bar 菜单,作为一个不使用 menu-bar 的人来说这点可以算是无用功了。
;; 如果你不需要数据持久化,那么就不需要下面的 hook 了
(use-package filesets
:ensure nil
:commands filesets-run-cmd ;; ...
:hook (kill-emacs . filesets-save-config))
因为 filesets 库里默认标记 autoload
的只有 filesets-init
函数, 而我们又没有使用这函数,导致 filesets 无法被动加载,于是只能手动导出需要的命令了。
然后就可以通过 M-x filesets-add-buffer
来将当前 buffer 加入至一个 filesets 组中;使用 M-x filesets-remove-buffer
来将当前 buffer 从 filesets 组删除。需要注意,filesets 会使用 custom system 将组信息 (filesets-data
) 持久化。如果不想弄脏 custom.el
的话可以不调用 filesets-save-config
。
个人觉得比较常用的命令就只有如下几个:
filesets-run-cmd
(属Run Shell Command
最常用)filesets-open
当 filesets 组过大时需注意filesets-close
剩下的都的都是 filesets 所提供的必要性功能。
filesets-edit
(使用 custom interface 编辑filesets-data
,不过直接编辑可能更快一点)filesets-add-buffer
filesets-remove-buffer
filesets-save-config
由于 Emacs 本身就提供了 multi-isearch-files
, multi-isearch-files-regexp
和 multi-occur
等函数,用 filesets 的好处则是将操作的文件组给记录下来方便后期的操作。
Shadow files 可以算是 Emacs 内的单向同步机制。例如在本地写完代码后想上传至服务器,那么就可以通过这个机制来完成。为什么不直接用 tramp 呢?因为 lsp-mode 默认没法在 tramp 上补全,还需另外设置,而且 tramp 在每次保存文件的时候都会同步一次,shadow file 则是将此文件加入到写队列中,最后通过 M-x shadow-copy-files
来同步。
(use-package shadowfile
:ensure nil
:config
(shadow-initialize)
(setq shadow-literal-groups
'(("/Youmu:/tmp/a.cpp" ;; 本机,本机前缀需要跟 `shadow-system-name' 一样
"/ssh:the-remote-machine:/tmp/a.cpp" ;; 跟 tramp 的格式一样
))))
shadow-literal-groups
用于单文件的同步,如上配置表示本机和 the-remote-machine
的 /tmp/a.cpp
需要同步。尝试一下从本机同步到远程的机器上,在 /tmp/a.cpp
里随便写入点东西。由于前面已经调用过 shadow-initialize
了,所以当一个文件需要同步的时候,会在 C-x C-s 时弹出提醒 Use C-x 4 s to update shadows
, 当然也可以直接调用 M-x shadow-copy-files
. shadowfile
同步依赖 tramp,所以无法避免 tramp 自身卡顿的问题,但是它可以避免通过 tramp 直接编辑时频繁保存带来的同步问题。
上图显示 shadowfile
成功地将本地的文件同步至了远端。
当然如果仅仅是同步一个文件,那么简单的 scp 即可,也没必须引入这么多的复杂度了。
(use-package shadowfile
:ensure nil
:config
(shadow-initialize)
(setq shadow-regexp-groups
'(("/Youmu:\\`/tmp/shadow/.+\\.[ch]pp\\'" ;; 本机
"/ssh:the-remote-machine:\\`/tmp/shadow/.+\\.[ch]pp\\'"))))
如上这个例子可能更符合日常用途,它默认将本机上的 /tmp/shadow/
下的 hpp/cpp 文件与远端机器同步。这里需要注意,如果远端没有目录则需要自己提前创建一个不然会同步失败。
其实 shadow 有提供命令来构建规则
shadow-define-literal-group
shadow-define-regexp-group
在使用命令的时候需要注意, shadow 里的 SITE
是一个 /ssh:the-remote-machine:
这种形式的字符串,如果想指代本机的话用 shadow-system-name
的值就行了。