Skip to content

Latest commit

 

History

History
1360 lines (1000 loc) · 55.7 KB

introduction-to-builtin-modes.md

File metadata and controls

1360 lines (1000 loc) · 55.7 KB

Emacs builtin modes 功能介绍

Emacs自带的mode功能也比较强大,而一般初学者(比如我)使用Emacs时间较短,对 它自身强大的mode不了解而错失一些可以提高生产力的工具。

winner-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)

winner-undo 搭配 transient-map

如果平常有注意观察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

saveplace记录了上次打开文件时光标停留在第几行、第几列。如果不想每次打开文件都 要再次跳转到上次编辑的位置,这个mode可以轻松地应对这种情况。

建议配置:

(use-package saveplace
  :ensure nil
  :hook (after-init . save-place-mode))

recentf

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_EDITMSGmagitcommit message时的文件,记录此文件无意义,故除外。

hl-line

高亮当前行。

(use-package hl-line
  :ensure nil
  :hook (after-init . global-hl-line-mode))

repeat-mode

如果有订阅 emacs-devel 邮件列表的话会发现曾经有一个投票令 C-x o 调用的是一个 transient-map,后续再切换窗口可以直接用 o 来切换。POLL: make C-x o transient

这样的一个好处是可以减少 Ctrl 按键的使用,降低 RSI 的风险;此外, transient-map 下还有一个 keymap 可以自定义,扩展性就更强了(例如可以在这个 keymap 上再定义 Obackward-other-window 函数)。

在最终的实现里则是新增了一个 repeat-mode, 这个 minor-modepost-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 { { { 来缩小窗口,其他命令类似
  • undoC-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

如果你想要一个足够简单的注释与反注释功能,那么自带的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函数:

  1. comment-dwim
    • 当用户选中区间时,会在对应区间注释或者反注释
    • 如果当前行是空的,那么会插入一个注释并且将它对齐
    • 如果使用C-u前缀,会则调用comment-kill来删除这个注释
    • 其他情况下则调用comment-indent在尾部插入注释并对齐
  2. comment-line
    • 当用户选中区间时,会在对应区间再加上下一行进行注释或者反注释
    • 如果当前行是空的,那么只会跳到下一行不会插入注释
    • 其他情况下则会将当前行注释或者反注释并跳到下一行
  3. comment-box 看例子就行
(defun add (a b)
  (+ a b))

;;;;;;;;;;;;;;;;;;;;;;
;; (defun add (a b) ;;
;;   (+ a b))       ;;
;;;;;;;;;;;;;;;;;;;;;;

hideshow

隐藏、显示结构化数据,如{ }里的内容。对于单函数较长的情况比较有用。

建议配置:

(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的插件

其中origamilsp支持lsp-origami

hideshow 扩展: 显示被折叠的代码行数

默认情况下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)

附效果图:

before-fold after-fold

whitespace

显示空白字符,如\t \f \v 空格等等。

可以配置在prog-modemarkdown-modeconf-mode下,显示行尾的空白字符。

(use-package whitespace
  :ensure nil
  :hook ((prog-mode markdown-mode conf-mode) . whitespace-mode)
  :config
  (setq whitespace-style '(face trailing)))

当然,仅显示行尾空白字符也可以简单地设置show-trailing-whitespacet来开启。

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)
     )))

比较好的是能指示过长的行,这样都不需要装那种显示一条竖线的插件了。

效果图

so-long

有时候会打开一些文件,这些文件里的某一行特别长,而Emacs没有针对这种情况做特殊 处理,会导致整个界面卡死。现在它来了!

直接全局启用:

(use-package so-long
  :ensure nil
  :config (global-so-long-mode 1))

当打开一个具有长行的文件时,它会自动检测并将一些可能导致严重性能的mode关闭, 如font-lock (syntax highlight)。

注:Emacs 27+ 自带

glasses

当遇到驼峰式的变量时,如CamelCasesName,但是你比较喜欢GNU式的命名方式(使用 下划线),那么你可以开启glasses-mode。它只会让CamelCasesName显示Camel_Cases_Name而不会对原文件做出修改。

不过,大写字母加下划线的组合有点奇怪。

subword

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

如果你的屏幕很宽,但是实际显示的条目的宽度无法利用这宽屏幕,那么follow-mode可 以帮助你。一个典型的使用案例是,再打开一个窗口,然后对当前buffer开启 follow-mode,这样之后另一个窗口显示的内容会是当前窗口的后续。例如,一个文件有 100行,当前buffer只能显示10行,那么另一个窗口将会显示下面10行。如果嫌窗口数还 是太少,可以继续增多。

follow-mode

delsel

Kermit95分享。

选中文本后,直接输入就可以,省去了删除操作。这在其他文本编辑器里都是标配,建议打开。

(use-package delsel
  :ensure nil
  :hook (after-init . delete-selection-mode))

parenthesis

高亮显示配对的( ) [ ] { } 括号,比较实用,建议打开。

(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

simple包提供的基础命令非常多,这里会慢慢完善。

modeline里显示行号、列号以及当前文件的总字符数。

(use-package simple
  :ensure nil
  :hook (after-init . (lambda ()
                         (line-number-mode)
                         (column-number-mode)
                         (size-indication-mode))))

shell-command-on-region的妙用

在注释中看到不认识的单词,一般做法是复制、打开终端、然后调用外部程序 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等一系列函数!

autorevert

有时候Emacs里打开的文件可能被外部修改,启用autorevert的话可以自动更新对应的 buffer.

(use-package autorevert
  :ensure nil
  :hook (after-init . global-auto-revert-mode))

isearch

本身Emacs自带的isearch已经足够强大,稍加修改就可以增加实用性。

例如anzu的显示匹配个数的功能就已经原 生支持了。通过

(setq isearch-lazy-count t
      lazy-count-prefix-format "%s/%s ")

来显示如 10/100 这种状态。

比较恼人的一点是,在搜索中删除字符会回退搜索结果,而不是停在当前位置将最后一个搜 索字符删除。这里可以通过remap isearch-delete-char来实现。

此外,还可以将搜索结果保持在高亮状态以方便肉眼识别。这个是通过设置 lazy-highlight-cleanupnil实现的。去除高亮状态需要人工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-countlazy-count-prefix-format需要Emacs 27+

令 isearch 像在浏览器里搜索一样

在浏览器里,我们只需要按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

tempo可以算是yasnippet的祖先, skeleton算是它的爷爷。由于tempo里可以使用elisp函数,灵活性非常大。

实际上在写代码的时候,想插入一个LICENSE头是个比较常用的需求,它也可以通过其 他方式如auto-insert在打开文件时就自动插入。在这里,我们使用tempo来实现。

目前比较推荐的方式是采用SPDX的格式,而不是直接把license内容写入代码文件中。 采用SPDX格式可以有效的减少文件大小,不会喧宾夺主占用大量代码行数。

一个典型的license头是这样:

// Copyright 2017 - 2018 ccls Authors
// SPDX-License-Identifier: Apache-2.0

所以我们可以仿照着这个格式来写一个tempotemplate.

;; 完整的列表非常长,可以访问 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体验一下。

align

听说有些写java的朋友特别喜欢将变量的=对齐,即原来的代码是这样的:

private int magicNumber = 0xdeadbeef;
private double PI = 3.14159265358939723846264;

选中它们,然后调用align-regexp,给定=作为它的参数,就会将上述代码的=部分对 齐了。

private int magicNumber = 0xdeadbeef;
private double PI       = 3.14159265358939723846264;

其他align相关的函数功能还有待开发。

dired

dired是一个用于directory浏览的mode,功能非常丰富。因此这里介绍的东西肯定不 能完全覆盖,会慢慢完善之。

在 dired 中用外部程序打开对应文件

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-commanddired-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 (sdired-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

RET 后仅保留一个 dired buffer

之前要完成这个功能,需要将 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

ispell全称是interactive spell检查器,它支持ispell, aspellhunspell, 以下以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/

calendar

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-add-entry-to-org-agenda-diary-file

org-agenda-diary-file

需要注意,它默认不会自动保存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了?

strokes

如果你想用鼠标来控制Emacs的行为,有点像现在浏览器上的鼠标手势。不过它只能识别 鼠标移动轨迹所描绘的形状,不能判断它的方向。

  1. 执行strokes-mode打开minor-mode
  2. 执行strokes-global-set-stroke在弹出的buffer内使用Shift+鼠标左键(也可以 用中键)绘出想作为快捷操作的大致形状,假设是一个 C 的形状,然后鼠标右键结束绘 制。稍后会提示输入与stroke对应的命令,假设是strokes-help
  3. 移动鼠标,使得它的轨迹是个 C 的形状
  4. Shift+鼠标中键以执行与这个stroke对应的命令,也就是strokes-help

想要更详细的信息?请M-x strokes-help.

webjump

你想在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 小技巧

因为transient-map的优先级比其他keymap都要高,所以可以将它当作菜单来使用。

如果嫌set-transient-map用起来不方便,可以使用hydra代替。

编辑时拷贝 (配合 avy)

如果在编辑文字时发现要拷贝一个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 之一。

  1. 首先将光标移动到想复制的url
  2. 再将这个url复制到kill-ring当中
  3. 再回到原来的位置
  4. 再粘贴
(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来偷懒一下。

效果图:

copy-url-at-point

type-break

历史老物,1994 年的时候就已经出现了。

打字打累了,想休息一下?看代码看累了,想放松一下?

那么它可能会适合你。如果在一段时间内的敲击键盘次数大于阈值,那么它会假设平均速度35 wpm,每个单词长度5来推算出要休息多少分钟。

而到达休息状态时,它可能会显示出一个汉诺塔移动的动画。可以M-x type-break立即体验!

timeclock

这是一个计算时间到底去哪里了的包,不过都有org-mode了,真的还会有人来用这个吗?

org timeclock
org-clock-in timeclock-in
org-clock-out timeclock-out

功能与org-mode几乎一致,不过它可以随时timeclock-out不用管记录时间的文件打开与否,而在org-modeclock-out则要保证运行clock的那个文件还处于打开状态。

elide-head-mode

依旧是怀旧向的内置包,可以将源代码文件头部中大量的license说明折叠起来,效果跟hideshow包类似。可以通过配置elide-head-headers-to-hide来自定义想要的折叠区间。最近在 Emacs 29 中引入了 elide-head-mode minor mode, 这样就不必再手工调用 elide-headelide-head-show 了。这两个函数已经被标记为 obsoleted 了。

midnight 深夜模式

在晚上零点的时候定期执行一些任务,默认是clean-buffer-list,可以设置midnight-hook来自定义行为。

M-x midnight-mode 来开启深夜模式。嗯,又到了深夜网抑云音乐时间了。

term 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

term-mode 算是一个完整的终端模拟器,与外部的终端模拟器相比除了刷新速度慢、色彩显示较差之外就没有其他差别了。因此如果是在 Linux/MacOS 平台下且只在本地使用,是比较推荐 term-mode 的。term-mode 分别可以通过 termansi-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 就好。

htop in term-mode

term-mode vs alacritty

这里不得不提一下, 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-pC-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 变量。

directory track in term-mode

另外一种方式则是依赖 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 不同,这点需要注意。

hacking 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-mapC-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-mode 它实际上不算是一个终端模拟器,它只是简单包装了一下 shell, 所以只能执行一些简单的命令, htop 这种存在复杂交互的应用就不行了。它也支持上下跳转到 prompt 处,而且它的默认值足够通用,如果不适用的话用户再自己配置一下 shell-prompt-pattern. 通过 C-c C-pC-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 vs term-mode

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 执行。

shell repl

eshell

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-subcommandseshell-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 的路。

因为 eshellshell-mode 都使用了 pcomplete, 所以这两者都能够享受到由此带来的补全效果。

默认情况下,在 eshellC-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 保持一致。

文件的艺术

file cache

如果在 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

注意,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 中。

filesets

如果你频繁地在一些 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-regexpmulti-occur 等函数,用 filesets 的好处则是将操作的文件组给记录下来方便后期的操作。

Shadow files

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-sync-from-local-to-remote

上图显示 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 文件与远端机器同步。这里需要注意,如果远端没有目录则需要自己提前创建一个不然会同步失败。

shadowfile-sync-from-local-to-remote-using-regexp

其实 shadow 有提供命令来构建规则

  • shadow-define-literal-group
  • shadow-define-regexp-group

在使用命令的时候需要注意, shadow 里的 SITE 是一个 /ssh:the-remote-machine: 这种形式的字符串,如果想指代本机的话用 shadow-system-name 的值就行了。