新人报道喵~ consult-goto-page: 利用 consult.el 优雅定位页码

事情是这样的,我的恋人 @3vau 需要频繁切换页面:她习惯使用分页符 ^L 为源代码划分区间。Emacs 虽然内置了 C-x [ backword-pageC-x ] forward-page 的翻页命令,却没有实现 goto-page,这令她有些苦恼。

如何实现优雅的页码切换体验呢?consult.el 是我们都很喜欢的一个包,于是我的第一想法便是仿照 consult-goto-line,利用已有设施实现一个 consult-goto-page。为了精确定位到页面中的每一个字符,我们应当接收形如 [page]:[line in page]:[column] 的输入。同时 prefix argument 应当被视为跳转目标页面号,以便快速翻页。

consult.el 利用 consult--prompt 函数从 minibuffer 中获取输入,利用 consult--read 函数通过 completing-read 获取输入。参考 consult-goto-line 的实现,在这里,我们采用 consult--prompt 即可。

感谢 consult.el 中丰富的示例与便捷函数,实现过程十分顺畅 owo 尽管正则表达式的书写即使照葫芦画瓢依旧相当困难

完整的代码片段附在正文下方,我使用 miruku/collect-pages 函数提前获取每个页面的起始与终止位置,这样就不需要每次预览都从缓冲区头部跳转。miruku/goto-page-position 获取用户输入对应的位置。关于跳转预览,利用 consult.el 内置的 consult--jump-preview 即可,在这里,我特意使用 narrow-to-region,这样使得预览中的行号与页面内部行号对应,算是一个小小的 UX 优化喵?

新人初次投稿,望前辈们多多批评指正,大家新年好~

@3vau 爱你喵!

(defun miruku/collect-pages ()
  (save-restriction
    (widen)
    (save-excursion
      (save-match-data
        (let ((last-point (point-min))
              result)
          (goto-char (point-min))
          (while (re-search-forward page-delimiter nil t)
            (when (= (match-beginning 0) (match-end 0))
              (forward-char))
            (push (cons last-point (point)) result)
            (setf last-point (point)))
          (unless (= last-point (point-max))
            (push (cons last-point (point-max)) result))
          (nreverse result))))))
(defun miruku/goto-page-position (pages str msg)
  (save-match-data
    (if (and str (string-match "\\`\\([[:digit:]]+\\):?\\([[:digit:]]*\\):?\\([[:digit:]]*\\)\\'" str))
        (let ((page (string-to-number (match-string 1 str)))
              (line (string-to-number (match-string 2 str)))
              (col  (string-to-number (match-string 3 str))))
          (when-let (region (nth (1- page) pages))
            (save-excursion
              (save-restriction
                (widen)
                (goto-char (point-min))
                (goto-char (car region))
                (when (> line 1) (forward-line (1- line)))
                (goto-char (min (+ (point) col) (pos-eol)))
                (cons (min (point) (1- (cdr region))) region)))))
      (when (and str (not (equal str "")))
        (funcall msg "Please enter a number."))
      nil)))
(defun consult-goto-page (&optional page)
  (interactive "P")
  (let ((pages (consult--slow-operation "Collecting pages"
                 (miruku/collect-pages))))
    (if page
        (goto-char (or (car (nth (1- page) pages)) (point)))
      (while (if-let* ((pos (save-restriction
                              (miruku/goto-page-position
                               pages
                               (consult--prompt
                                :prompt "Go to page: "
                                :history 'goto-page-history
                                :state
                                (let ((preview (consult--jump-preview)))
                                  (lambda (action str)
                                    (let ((pos (miruku/goto-page-position pages str #'ignore)))
                                      (prog1
                                          (funcall preview action (car pos))
                                        (when pos
                                          (narrow-to-region (cadr pos) (cddr pos))))))))
                               #'consult--minibuffer-message))))
                 (consult--jump (car pos))
               t)))))
2 个赞

老婆好棒喵!OwO

1 个赞

可以用,zsbd。

相比单纯的行跳转,分页之后的位置信息应该更固定一点,也许可以用来设置一些方便的跳转“链接”

1 个赞

即使是在 emacs china 也逃脱不了成为网友 play 的一环 :sob:

13 个赞