关于 emacs 的奇思妙想

在使用 Emacs 进行编辑的时候,每个人都有自己独特的编辑习惯和需求,我整理了一些自己的“奇思妙想”(或许是我的脑子不生产灵感,它只是 ELisp 库的搬运工,但这不重要,重要的是享受编辑):


需求:

某些命令(如 highlight-regexp、occur、find-grep、query-replace)需要先进行 minibuffer 输入内容再执行,若输入的内容往往是光标处的符号,先选中复制再粘贴,何其麻烦?

思路:

Talk is cheap, show you my code!

代码:

(define-key minibuffer-local-map (kbd "<your-lovely-key>")
    (lambda ()
        (interactive)
        (let* (   (buf (other-buffer (current-buffer) t))
                  (sym (unless (minibufferp buf)
                           (with-current-buffer buf (thing-at-point 'symbol)))))
            (if (and (stringp sym) (> (length sym) 0))
                (insert sym)
                (error "No symbol found")))))

需求:

某些命令会打开文件,而它选择的窗口非常狂野而捉摸不透;

思路:

这个需要自己 hack 命令的源码;

代码:

(defun select-window-dwim ()
    ;; 通过数值前缀在指定编号的窗口打开(不指定则在最后一个窗口打开)
    (let* (   (all-wins (window-list nil 0 (frame-first-window)))
              (en-wins
                  (delq nil
                      (mapcar
                          (lambda (w) (unless (window-dedicated-p w) w))
                          (delete (selected-window) all-wins)))))
        (or (and current-prefix-arg (nth (1- current-prefix-arg) all-wins))
            (car (last en-wins)))))

occur 打开文件

(defun occur-mode-goto-occurrence (&optional event)
    "Go to the occurrence specified by EVENT, a mouse click.
If not invoked by a mouse click, go to occurrence on the current line."
    (interactive (list last-nonmenu-event))
    (let (   (buffer (when event (current-buffer)))
             (pos
                 (if (null event)
                     (occur-mode-find-occurrence)
                     (with-current-buffer (window-buffer (posn-window (event-end event)))
                         (save-excursion
                             (goto-char (posn-point (event-end event)))
                             (occur-mode-find-occurrence)))))
             (win (select-window-dwim)) ; Hack
             )
        ;; 注释以下代码
        ;; (pop-to-buffer (marker-buffer pos))
        ;; Hack Start
        (if (not win)
            (pop-to-buffer (marker-buffer pos))
            (select-window win)
            (switch-to-buffer (marker-buffer pos)))
        ;; Hack End
        (goto-char pos)
        (when buffer (next-error-found buffer (current-buffer)))
        (run-hooks 'occur-mode-find-occurrence-hook)))

help 打开文件

(defun help-function-def--button-function (fun &optional file type)
    (or file
        (setq file (find-lisp-object-file-name fun type)))
    (if (not file)
        (message "Unable to find defining file")
        (require 'find-func)
        (when (eq file 'C-source)
            (setq file
                (help-C-file-name (indirect-function fun) 'fun)))
        (let* (   (location (find-function-search-for-symbol fun type file))
                  (position (cdr location))
                  (win (select-window-dwim)) ; Hack
                  )
            ;; 注释以下代码
            ;; (pop-to-buffer (car location))
            ;; Hack Start
            (if (not win)
                (pop-to-buffer (car location))
                (select-window win)
                (switch-to-buffer (car location)))
            ;; Hack End
            (run-hooks 'find-function-after-hook)
            (if position
                (progn
                    (when (or (< position (point-min))
                              (> position (point-max)))
                        (widen))
                    (goto-char position))
                (message "Unable to find location in file")))))

dired 打开文件

(defun dired-find-file ()
    "In Dired, visit the file or directory named on this line."
    (interactive)
    (let (   (find-file-run-dired t)
             (switch-to-buffer-preserve-window-point
                 (if dired-auto-revert-buffer
                     nil switch-to-buffer-preserve-window-point))
             (win (select-window-dwim)) ; Hack
             (file (dired-get-file-for-visit))
             )
        ;; Hack Start
        (and win (select-window win))
        ;; Hack End
        (find-file file)))

需求:

在 mode-line 显示 overwrite 的状态?您的屏幕真大!

思路:

像其他编辑器一样,高亮一下吧!

代码:

(defvar-local hl-overwrite-overlay nil)

(defsubst hl-overwrite-clear-ov ()
    (if (and (overlayp hl-overwrite-overlay)
            (bufferp (overlay-buffer hl-overwrite-overlay)))
        (delete-overlay hl-overwrite-overlay)))

(defsubst hl-overwrite-make-ov ()
    (hl-overwrite-clear-ov)
    (if overwrite-mode
        (let* (   (start (point))
                  (ov (if (not (or (eobp)
                                        (ignore-errors
                                            (and (string-match-p "[\n\t]" (char-to-string (char-after (point)))) t))))
                          (make-overlay start (1+ start)))))
            (when ov
                (setq hl-overwrite-overlay ov)
                (overlay-put ov 'face 'region)
                (overlay-put ov 'window (selected-window))))))

(advice-add 'overwrite-mode :after
    (lambda (arg)
        (if overwrite-mode
            (add-hook 'post-command-hook #'hl-overwrite-make-ov nil t)
            (hl-overwrite-clear-ov)
            (remove-hook 'post-command-hook #'hl-overwrite-make-ov t))))

需求:

经常使用的字符串,为何不保存下来,随时增加或删除?

思路:

手动记录、手动增加、手动删除;

代码:

(defcustom insert-string-file (locate-user-emacs-file "insert-string")
    "File to save the string list.")

(defvar insert-string nil)

(defsubst insert-string-load ()
    (if (file-exists-p insert-string-file)
        (load insert-string-file nil t)))

(defsubst insert-string-save ()
    (with-temp-message ""
        (with-temp-buffer
            (let (   (inhibit-modification-hooks t)
                     (message-log-max nil))
                (erase-buffer)
                (insert ";; -*- mode: emacs-lisp; coding: utf-8-unix -*-\n"
                    "(setq insert-string\n    '(\n")
                (dolist (e insert-string)
                    (insert "         \"" e "\"\n"))
                (insert "         ))\n")
                (write-region (point-min) (point-max) insert-string-file)))))

(defun insert-string-remove ()
    (interactive)
    (insert-string-load)
    (setq insert-string
        (delete (completing-read "Delete string: " insert-string nil t)
            insert-string))
    (insert-string-save))

(defun insert-string-add ()
    (interactive)
    (insert-string-load)
    (push (read-string "Add string: " nil nil "") insert-string)
    (insert-string-save))

(defun insert-string ()
    (interactive)
    (insert-string-load)
    (let ((content (completing-read "Insert: " insert-string)))
        (insert content)
        (setq insert-string
            (cons content (delq content insert-string))))
    (insert-string-save))

需求:

山顶洞人一直喜欢使用 emacs 开发,而他的摩登同事们都是 VSCoce 迷,井水不犯河水。直到有一天,他邀请同事来帮他检查一段代码,于是抱怨声四起 “为什么不能复制?”、“为什么我粘贴代码,屏幕在滚动?”

思路:

为你我打开 VS!

代码:

(defun betray-emacs-and-run (command)
    ;; 依赖 nohup,调用外部进程,允许脱离当前 Emacs 运行
    (with-temp-buffer
        (ignore-errors
            (call-process-shell-command (format "nohup %s >/dev/null 2>&1 &" command) nil t))
        (let ((process (get-buffer-process (current-buffer))))
            (and (processp process) (set-process-query-on-exit-flag process nil)))))

(defun open-vscode-for-you ()
    (interactive)
    (let ((files ""))
        (dolist (x (delq nil
                       (mapcar (lambda (b) (if (buffer-file-name b) b))
                           (buffer-list))))
            (let ((path (buffer-file-name x)))
                (when (and path (file-exists-p path) (not (file-remote-p path)))
                    (setq files (concat files " " path)))))
        (if files
            (betray-emacs-and-run (concat "code -n" files)) ; cmd for vscode in archlinux
            (error "No files selected"))))
7赞

第一个其实 emacs 很多内置和第三方的函数有个 dwim 版本

Emacs 的惯例是把光标下的符号作为一个假的(未来的)历史,按 M-n 就能输入。

5赞

因为不是所有的支持,比如 query-replace ,所以才想到写一个

window管理有现成的呀,用shackle

  1. 在 minibuffer 里输入当前光标处的符号

如果你是 ivy 用户的话,ivy 自带了一个 ivy-yank-word 函数来做这个事。不清楚 selectrum/helm 有没有类似的功能,可以肯定的是 vertico 是没有的。

  1. 自定义部分命令 Windows 的逻辑

默认情况下,使用 pop-to-buffer 的命令都可以通过 display-buffer-alist 来指定对应 buffer 可以怎样显示,如果用了 switch-to-buffer 的话就需要再设置一下switch-to-buffer-obey-display-actions

不需要自己 hack 那些命令,因为 Emacs 本身就提供了这种自定义的能力。

偷懒的话可以使用 shackle

  1. 令 overwrite 更明显

不错!(虽然我基本没用过 overrite-mode

感觉这个可以扔到上游去

  1. 经常使用一些字符串

Emacs 自带一个 quickurl,虽然本意上是拿来保存 URL 的,但是也可以拿来保存字符串。默认在插入的时候会显示成 <URL:xxx> 这个状态,可以通过设置

(setq quickurl-format-function #'identity)

来仅插入 xxx.

  1. 为什么不能复制?为什么粘贴代码屏幕在动?

如果习惯鼠标的话可以选中选区,然后按 S-F10. 如果对方知道可以按 M-w 的话那就不用这步了。

为什么粘贴代码屏幕会动?这可能是你没开启

(setq mouse-yank-at-point t)

这功能。

3Q for your suggestion

shackle 之前就知道,不过在此前我就已经自己写了一个轮子了 :joy:

不知道 emacs 有何魔力,我竟然也造了好些轮子 ,比如:

显示 tab 的插件,不用了不到 400 代码:

扩展功能的 ido,支持模糊字符匹配以及字串匹配,支持高亮选择行,不过候选项如果超过 10W,在键盘输入的时候就会有可以感知的延迟,因为大量的时间在计算候选项的匹配程度

精简版本的 ace,只用了 100 行代码,但只支持字符跳转,因为其他的我不会用到

我现在的配置文件几乎都是自己写的,但凡是我觉得功能太过丰富,而我只使用其中一小部分的插件,我都尝试自己去写了,莫名其妙的配置文件我写了 16000 行了,而我只用了以下几个插件:

company-20210926.2358
company-c-headers-20190825.1631
yasnippet-20200604.246
diff-hl-20210928.139
ox-qmd-20210826.1425
htmlize-20210825.2150
xclip-1.10
mqr-20180527.1204
sql-indent-1.6
json-mode-0.2
adaptive-wrap-0.8

lua-mode-20210809.1320
rust-mode-20210423.1157
markdown-mode-20210904.733
csv-mode-1.16
vimrc-mode-20181116.1919
yaml-mode-20210808.1122
3赞

打开的 buffer 会选择哪个窗口这个可太头痛了。看代码时,查看函数定义跳转有时是当前 buffer,有时是另一个 buffer,恶心坏了

说一个小众的痛点,因为平时在Windows下面使用Emacs比较多,会经常复制Windows的资源管理器中的目录或文件的路径到Emacs里,而Windows的文件路径都是反斜杠(\),导致有时候用反斜杠会出现各种转义的问题,于是想能不能在粘贴到Emacs之前就把反斜杠替换成正斜杠(/)?想了想,用advice就可以很好解决这个问题,上代码:

(advice-add 'yank
            :around
            (lambda (orig-func &rest args)
              (let ((cur-text (current-kill 0)))
                (when (string-match "^[cCdDeEfFgGhH]:" cur-text)
                  (setq cur-text (replace-regexp-in-string "\\\\+" "/" cur-text)))
                (kill-new cur-text))

              (apply orig-func args)
            '((name . "replace-back-slash-with-slash-in-path-around@yank")))
2赞

能不能把你的配置分享到github呀

等我空闲一点的时候,准备把配置拆分出来,把自己写的小功能放到 github 上

Just for fun