事情是这样的,我的恋人 @3vau 需要频繁切换页面:她习惯使用分页符 ^L 为源代码划分区间。Emacs 虽然内置了 C-x [ backword-page 与 C-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)))))
