Emacs builtin mode 功能介绍

好文章好文章,感谢

hydra-copy 这个注意不错。

我试了一下,个人感觉先跳位置、再选类型比较顺手。

我常年养成的习惯是,先盯住要选的位置,然后快捷键呼出 avy,输入高亮字符。此时如果 minibuffer 先弹出个窗口,眼睛会不自觉地往下瞄一下,这样先前盯住的焦点就跑了,需要眼睛再次搜索。如果先跳好位置就跑不掉了,可以从容思考拷贝什么类型。

方法定义如下:

(defun avy-copy-thing-at-point ()
  "Copy thing at point."
  (interactive)
  (save-excursion
    (avy-goto-word-or-subword-1)
    (let ((thing
           (cl-case (read-char
                     (format
                      "Copy thing at point (%s: word %s: symbol %s: list %s: url): "
                      (propertize "w" 'face 'error)
                      (propertize "s" 'face 'error)
                      (propertize "l" 'face 'error)
                      (propertize "u" 'face 'error)))
             (?w  'word)
             (?s  'symbol)
             (?l  'list)
             (?u  'url))))
      (kill-new (thing-at-point thing))
      (message "%s copied" thing))))

如果想要美观一点就换 hydra,我这里先用 read-char 顶着。

1 个赞

下面这个提示用的熟了之后可以直接无视,因为绑定的前缀按键跟单词的前缀一样。

刚写了一个包avy-thing-edit,给懒猫大神的thing-edit 加入了avy-jump 功能,对于远处的目标可以很方便地copy/cut/replace

2 个赞

Changelog:

  • 2020-09-04 newcomment 模块注释与反注释
  • 2020-09-04 打字打累了?看一下type-break
  • 2020-09-04 通过timeclock来管理时间
  • 2020-09-18 增加elide-head的介绍
  • 2020-09-19 增加midnight-mode的介绍

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

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

依旧是怀旧向的内置包,可以将源代码文件的头部中大量的license说明折叠起来,效果 跟hideshow包类似。可以通过配置elide-head-headers-to-hide来自定义想要的折叠区间。

midnight 深夜模式

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

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

11 个赞

发现一个可以给 evil-mode 实现相对行号的方法:

;;; relative line number
(defun my-enable-relative-line-number ()
  (interactive)
  (display-line-numbers-mode -1)
  (setq display-line-numbers-type 'relative)
  (display-line-numbers-mode 1))

(defun my-disable-relative-line-number ()
  (interactive)
  (display-line-numbers-mode -1)
  (setq display-line-numbers-type t)
  (display-line-numbers-mode 1))

(add-hook 'evil-normal-state-entry-hook 'my-enable-relative-line-number)
(add-hook 'evil-insert-state-entry-hook 'my-disable-relative-line-number)
(add-hook 'evil-insert-state-exit-hook 'my-enable-relative-line-number)
3 个赞

Deferred-Eval 如果一个计算结果在未来不必需要,那么您应该想避免对其进行耗时的计算,这种情况下 对一个表达式的延迟求值就显得很有用了。 thunk.el 就是这样一个支持延迟求值的库。

;;; 只有当 derived-number 变量被调用时才会对其后跟着的表达式进行求值运算
(setq lexical-binding t)
(require 'thunk)
(defun f (number)
  (thunk-let ((derived-number
	       (progn (message "Calculating 1 plus 2 times %d" number)
		      (1+ (* 2 number)))))
    (if (> number 10)
	derived-number
      number)))
(f 5) ; ⇒ 5

(f 12); ⊣ Calculating 1 plus 2 times 12 ⇒ 25
(setq lexical-binding t)
(require 'thunk)
(thunk-let* ((x (prog2 (message "Calculating x...")
		    (+ 1 1)
		  (message "Finished calculating x")))
	     (y (prog2 (message "Calculating y...")
		    (+ x 1)
		  (message "Finished calculating y")))
	     (z (prog2 (message "Calculating z...")
		    (+ y 1)
		  (message "Finished calculating z")))
	     (a (prog2 (message "Calculating a...")
		    (+ z 1)
		  (message "Finished calculating a"))))
  (* z x))

;⊣ Calculating z...
;⊣ Calculating y...
;⊣ Calculating x...
;⊣ Finished calculating x
;⊣ Finished calculating y
;⊣ Finished calculating z
;⇒ 8

PS: 发完才发现是内置 mode 的介绍,囧…

1 个赞

补充一下,recentf 展示时,可以对文件名预处理,比如把家目录替换为 ~

  (add-to-list 'recentf-filename-handlers 'abbreviate-file-name)

之前一直都是手写的函数,方式如下(用了 ivy)

  (defun my/recentf-open ()
    (interactive)
    (let ((file (ivy-read "Find recent file: " (mapcar 'abbreviate-file-name recentf-list))))
      (if (find-file file)
          (message "Opening file %s" (abbreviate-file-name file))
        (message "Aborting"))))

7 个赞

接着更新啊lz

emacs 可以设置需要的内存来提高运行速度吗?很好奇

莫急,最近又攒了一点新的东西,周末再更新

可以的,这是变相的减少GC的次数

默认的 C-v/M-v 的行为是翻页,阅读起来不是很方便,重新绑定下面的函数,把翻页变为滚动会舒服很多

(defun my-scroll-up ()
  (interactive)
  (scroll-up 1))

(defun my-scroll-down ()
  (interactive)
  (scroll-down 1))

老哥来 PR 呀

还以为更新了呢 :eyes:

莫急,正在录视频中,这次会分享一下我是怎么从 vterm 切换至 ansi-term 的,以及我是如何使用 eshell, shell-mode

4 个赞

:+1:

期待新介绍,后面会讲 wdired 吗?最近在网上看到这么一篇 Working with multiple files in dired - Mastering Emacs 感觉放到前面讲过的 dired 里很不错。

Proced,类似top,

直接M-x proced即可,全平台可用,推荐配置:

(setq-default proced-auto-update-flag t ; 自动刷新
			  proced-auto-update-interval 3 ) ; 默认为5秒一次

viper-mode

Yet another vim emulator,没有visual-mode,ex命令支持不好,会干扰minibuffer的keymap(这个很要命),而且代码耦合严重,想自己改点都很麻烦.

但比evil轻一个量级,和很多内建mode配合得不错,对启动时间敏感又有点空闲时间的人可以花点时间调教一下.

建议看看官方指南

我觉得和内建mode配合得不错,是不是可以把它的代码给 port 到 evil-mode ?

Changelog:

  • 2020-12-14 term 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 就好。

这里不得不提一下, 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
}

其实它就是在每条命令执行前将自己当前的目录告诉了 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 的不同,这点需要注意。

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 等各种可能改变当前目录的各种命令来达到的。

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

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 保持一致。

17 个赞