Les Frites

fieryzig's blog

06 Mar 2021

关于dashboard中recent files的问题

最近在使用dashboard的时候,发现了一个不完美的地方,就是经常发现Recent Files里面会出现Org Agenda相关的文件。这些文件占据了Recent Files的位置,导致无法显示出我真正想要的最近文件。 搜索了一下,发现dashboard的issues里面已经有了相关的问题,并且!已经有PR修复了这个问题! 看来事情并不是这么简单。

问题的复现

因为自己只是在日常使用中经常遇到这个问题,并没有留意发生的条件,所以问题的第一步是复现这个问题。

首先发现重新启动Emacs显示的结果是正确的(Recent Files里面没有Org文件)

由于我的Emacs里面配置了 tempbuf ,所以10s钟后会自动kill掉org的buffer。

经过多次实验,我发现,10s前我在dashboard按 g ,刷新dashboard,显示正确。但是10s后(即org buffer被kill后),Recent Files里面就会出现Org文件。

一个问题此时变成了多个问题:

  • 为什么org buffer被kill后,刷新dashboard会显示错误?
  • 为什么启动Emacs又是正确的?
  • 怎么解决这个问题?怎么让dashboard一直显示正确?

继续探究

从dashboard出发

dashboard照理说已经修复了这个问题,我们先来看看PR83究竟是怎么修复的。 重点就在 dashboard-insert-startupify-lists 这个函数里

(defun dashboard-insert-startupify-lists ()
  "Insert the list of widgets into the buffer."
  (interactive)
  (let ((buffer-exists (buffer-live-p (get-buffer dashboard-buffer-name)))
        (recentf-is-on (recentf-enabled-p))
        (origial-recentf-list recentf-list)
        (dashboard-num-recents (or (cdr (assoc 'recents dashboard-items)) 0))
        (max-line-length 0))
    ;; disable recentf mode,
    ;; so we don't flood the recent files list with org mode files
    ;; do this by making a copy of the part of the list we'll use
    ;; let dashboard widgets change that
    ;; then restore the orginal list afterwards
    ;; (this avoids many saves/loads that would result from
    ;; disabling/enabling recentf-mode)
    (when recentf-is-on
      (setq recentf-list (seq-take recentf-list dashboard-num-recents)))
    (when (or (not (eq dashboard-buffer-last-width (window-width)))
              (not buffer-exists))
      (setq dashboard-banner-length (window-width)
            dashboard-buffer-last-width dashboard-banner-length)
      (with-current-buffer (get-buffer-create dashboard-buffer-name)
        (let ((buffer-read-only nil))
          (erase-buffer)
          (dashboard-insert-banner)
          (dashboard-insert-page-break)
          (setq dashboard--section-starts nil)
          (mapc (lambda (els)
                  (let* ((el (or (car-safe els) els))
                         (list-size
                          (or (cdr-safe els)
                              dashboard-items-default-length))
                         (item-generator
                          (cdr-safe (assoc el dashboard-item-generators))))
                    (add-to-list 'dashboard--section-starts (point))
                    (funcall item-generator list-size)
                    (when recentf-is-on
                      (setq recentf-list origial-recentf-list))
                    (setq max-line-length
                          (max max-line-length (dashboard-maximum-section-length)))
                    (dashboard-insert-page-break)))
                dashboard-items)
          (when dashboard-center-content
            (when dashboard--section-starts
              (goto-char (car (last dashboard--section-starts))))
            (let ((margin (floor (/ (max (- (window-width) max-line-length) 0) 2))))
              (while (not (eobp))
                (unless (string-suffix-p (thing-at-point 'line) dashboard-page-separator)
                  (insert (make-string margin ?\ )))
                (forward-line 1))))
          (dashboard-insert-footer))
        (goto-char (point-min))
        (dashboard-mode)))
    (when recentf-is-on
      (setq recentf-list origial-recentf-list))))

从中间的注释中可以看出,是先用 origial-recentf-listrecentf-list 记录下来,在最后再复原回去。

看起来好像没什么不对的。但是既然不对了,说明一定是 recentf-list 这个变量出了问题。并且 kill-buffer 之前没问题, kill-buffer 之后出了问题,是不是 kill-buffer 的时候改变了 recentf-list

到recentf中去

点这里看recentf源码

(defconst recentf-used-hooks
  '(
    (find-file-hook       recentf-track-opened-file)
    (write-file-functions recentf-track-opened-file)
    (kill-buffer-hook     recentf-track-closed-file)
    (kill-emacs-hook      recentf-save-list)
    )
  "Hooks used by recentf.")

从这里可以看出它的逻辑是 find-filewrite-file 的时候,将文件放入 recentf-list 中; kill-buffer 的时候将文件从 recentf-list 中删除;退出Emacs的时候将 recentf-list 写入文件。

看来这里也没什么问题。那问题究竟出在哪里?

一个猜想

先不考虑第一次启动Emacs的话,问题变得简单了一些。存在org文件buffer的时候,刷新dashbaord没问题。不存在org文件buffer的时候,刷新dashboard就会出错。所以,一定是刷新dashbaord的时候,agenda会打开org文件,如果org文件已经在buffer中了,就不会 find-file ;否则调用 find-file 就会触发 recentf-track-opened-file ,将文件加入到 recentf-list 中。

但是每次在刷新dashboard之后(无论是否正确),我将 recentf-list 打印出来,结果都是不包含org文件的。这应该是dashboard刷新后,用之前的origial的复原了 recentf-list

也就是 recentf-list 刷新之前和刷新之后都是好的。而最终展现的Recent Files却是错的。

真相只有一个了,那就是生成Recent Files的时候,用的 recentf-list 是错的。

深入dashboard-widgets

(defun dashboard-insert-recents (list-size)
  "Add the list of LIST-SIZE items from recently edited files."
  (setq dashboard--recentf-cache-item-format nil)
  (recentf-mode)
  (let ((inhibit-message t) (message-log-max nil))
    (recentf-cleanup))
  (dashboard-insert-section
   "Recent Files:"
   (dashboard-shorten-paths recentf-list 'dashboard-recentf-alist)
   list-size
   (dashboard-get-shortcut 'recents)
   `(lambda (&rest ignore)
      (find-file-existing (dashboard-expand-path-alist ,el dashboard-recentf-alist)))
   (let* ((file (dashboard-expand-path-alist el dashboard-recentf-alist))
          (filename (dashboard-f-filename file))
          (path (dashboard-extract-key-path-alist el dashboard-recentf-alist)))
     (cl-case dashboard-recentf-show-base
       (align
        (unless dashboard--recentf-cache-item-format
          (let* ((len-align (dashboard--get-align-length dashboard-recentf-alist))
                 (new-fmt (dashboard--generate-align-format
                           dashboard-recentf-item-format len-align)))
            (setq dashboard--recentf-cache-item-format new-fmt)))
        (format dashboard--recentf-cache-item-format filename path))
       (nil (format dashboard-recentf-item-format filename path))
       (t path)))))

在这里我们终于发现 recentf-list 。而这时的 recentf-list 应该是没有复原过的。所以才会输出错误的结果。

在这里,我们还注意到 (recentf-mode) 。也就是说 recentf-mode 是在这里启动的。这也是为什么重新启动Emacs显示正确的原因。因为dashboard先处理Agenda的org文件,此时还没有开启recentf-mode,所以结果是对的。

为了验证这一点,我将 dashbaord-items 里面的顺序调整了一下,把recentf放在agenda的前面。果然,再次启动Emacs,结果也是错误的。

结论

所以,这个PR压根没有修复Recent Files显示Agenda org files这个issue。它只在同时满足 首次启动Emacsagenda在recentf前面 这两个条件时,它才会Work。

最后说一下我的解决方案:

1.修改dashboard的逻辑

在上面dashboard的代码中加两行,作用是每次完成一个item之后,都复原一次 recentf-list

(mapc (lambda (els)
        (let* ((el (or (car-safe els) els))
               (list-size
                (or (cdr-safe els)
                    dashboard-items-default-length))
               (item-generator
                (cdr-safe (assoc el dashboard-item-generators))))
          (add-to-list 'dashboard--section-starts (point))
          (funcall item-generator list-size)
          (when recentf-is-on
            (setq recentf-list origial-recentf-list))
          (setq max-line-length
                (max max-line-length (dashboard-maximum-section-length)))
          (dashboard-insert-page-break)))
      dashboard-items)

这个方法,好处是简单容易,效率高,缺点是修改了dashboard,在代码没被合到master之前会比较难受。

2.重新定义g按键

(define-key dashboard-mode-map (kbd "g") #'my-dashboard-g)

my-dashboard-g 的实现有两种思路,一是在refresh之前,把recentf-mode关闭了。这个方法的缺点在上面的PR里面的注释里说道了,就是读写文件,效率差。二是直接refresh两次,因为第一次refresh的时候org文件的buffer是不在的,所以错了,第二次时org文件的buffer的文件还在,因此结果是正确的。

重新定义g按键的方法,并没有修复问题,只是看起来ok的trick,并且只能agenda在recentf的前面。

因此还是推荐第1种做法,改日有时间提个PR吧。

Tags: Emacs