问题
之前一直使用evil-collection包以完善evil-mode绑定在各个地方的一致性及覆盖率,特别的,evil-collection会为pdf-tools创建对应的evil-mode绑定,比如 /
和 ?
会绑定到 isearch-forward/isearch-backward
上,
n/N
则会绑定到 isearch-repeat-forward/isearch-repeat-backward
上,问题在于pdf-tools的整个搜索机制是基于isearch上实现的,故很多行为和isearch一致,但是和evil-mode用户的期望却不匹配,比如:
- 按
C-s
开始isearch搜索,然后输入关键字,此时默认匹配项会渐进高亮,这个没问题,问题是按回车后,isearch认为搜索结束,会直接清除高亮,这个行为可以通过 lazy-highlight-cleanup
来控制,问题是,pdf-tools直接禁用了isearch的lazy-highlight机制,导致 lazy-highlight-cleanup
无法控制该行为。由于按 /
也是调用
isearch-forward
进行搜索,因此在输入关键字后按回车,高亮一样会消失,这和平常evil-mode的行为是不一致的。
- 按
/
开始搜索,输入关键字,之后按回车后, isearch-mode
会被禁用,这之后按 n/N
调用 isearch-repeat-forward/isearch-repeat-backward
会进一步跳转到下一个匹配项,这点和预期一样,但是当搜索到PDF文件的下一页时,高亮会消失,出现该问题是因为,在搜索当前页时,pdf-tools不会redisplay
该页的内容,但是当搜索进行到PDF文件的下一页后,pdf-tools会进行redisplay,此时检测到 isearch-mode
没有启用,便不会进行高亮了。
- 绑定到
n/N
的命令 isearch-repeat-forward/isearch-repeat-backward
不会记住上一次的搜索方向。
思路及解决方案
我在Emacs China中发过完整的解决方案,这里主要讲下思路。
先解决在 /
搜索后,按回车,高亮会消失的问题,回答一个问题:为什么按回车后高亮会被清除?答案在于 pdf-isearch-mode-cleanup
函数,代码如下:
1
2
3
4
5
6
|
(defun pdf-isearch-mode-cleanup ()
"Cleanup after exiting Isearch.
This is a Isearch interface function."
(pdf-isearch-active-mode -1)
(pdf-view-redisplay))
|
该函数调用 pdf-view-redisplay
以redisplay当前页面,这时候高亮就没了。该函数会在 isearch-mode-end-hook
的时候被执行,如下:
1
|
(add-hook 'isearch-mode-end-hook 'pdf-isearch-mode-cleanup nil t)
|
而 isearch-mode-end-hook
会在isearch搜索结束后被触发,也就是按回车后的时候触发,这就是为什么按回车后,高亮会消失。我们期望的行为是:搜索如果匹配失败了,则回车后清除高亮,否则,回车后保持高亮,最简单、但是比较hack的方式就是覆盖 pdf-isearch-mode-cleanup
的实现,为此,我们先定义一个函数来判断isearch是否搜索匹配失败,如下:
1
2
|
(defun osfva-isearch-failed? ()
(or (not isearch-success) isearch-error))
|
接着,我们用advice覆盖 pdf-isearch-mode-cleanup
的实现,仅在搜索失败的时候,清除高亮,如下:
1
2
3
4
5
6
7
|
(defun osfva-pdf-isearch-mode-cleanup ()
"Don't cleanup highlight if search successfully."
(pdf-isearch-active-mode -1)
(when (osfva-isearch-failed?) (pdf-view-redisplay)))
(advice-add #'pdf-isearch-mode-cleanup
:override #'osfva-pdf-isearch-mode-cleanup)
|
解决完回车后清除高亮的问题后,我们继续解决第二个问题,即按 n/N
在搜索进行到下一页后,高亮会消失的问题,造成这个问题的原因是
pdf-isearch-hl-matches
,它的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
(defun pdf-isearch-hl-matches (current matches &optional occur-hack-p)
"Highlighting edges CURRENT and MATCHES."
(cl-check-type current pdf-isearch-match)
(cl-check-type matches (list-of pdf-isearch-match))
(cl-destructuring-bind (fg1 bg1 fg2 bg2)
(pdf-isearch-current-colors)
(let* ((width (car (pdf-view-image-size)))
(page (pdf-view-current-page))
(window (selected-window))
(buffer (current-buffer))
(tick (cl-incf pdf-isearch--hl-matches-tick))
(pdf-info-asynchronous
(lambda (status data)
(when (and (null status)
(eq tick pdf-isearch--hl-matches-tick)
(buffer-live-p buffer)
(window-live-p window)
(eq (window-buffer window)
buffer))
(with-selected-window window
(when (and (derived-mode-p 'pdf-view-mode)
(or isearch-mode
occur-hack-p)
(eq page (pdf-view-current-page)))
(pdf-view-display-image
(pdf-view-create-image data :width width))))))))
(pdf-info-renderpage-text-regions
page width t nil
`(,fg1 ,bg1 ,@(pdf-util-scale-pixel-to-relative
current))
`(,fg2 ,bg2 ,@(pdf-util-scale-pixel-to-relative
(apply 'append
(remove current matches))))))))
|
重点看那个lambda里面的内容,这个lambda会在 pdf-info-renderpage-text-regions
渲染完成后,异步执行,由于渲染是异步做的,因此渲染完成后,我们的PDF缓冲区、窗口可能已经被kill或者
delete掉了,甚至major-mode等被改变了,故它在lambda中做了一系列检查,比如 (buffer-live-p buffer)
、 (window-live-p window)
、
(derived-mode-p 'pdf-view-mode)
等等,检查通过才进行高亮,特别重要的一个就是,它检查了 isearch-mode
是否处于启用状态,然而前面说了,回车后, isearch-mode
会被禁用,故该判断会失败,不过注意到它还判断参数
occur-hack-p
是否为真,如果为真,则即使 isearch-mode
处于禁用状态时也进行高亮,那这个 occur-hack-p
是做什么的?这个参数实际上是供
pdf-isearch-occur
命令使用的,该命令可以在 C-s
或者 /
开始搜索后,输入关键字,然后不要按回车,按 M-s o
触发,它用 occur
以进行搜索和显示,会有一个专门的缓冲区列出PDF文件中所有匹配关键字的位置(整个文件,不止当前页),你可以在该缓冲区对应项按回车以跳转到对应页的对应位置,不仅如此,整个过程中当前页面的匹配项都是处于高亮状态的,这个
pdf-isearch-occur
会间接传递 occur-hack-p
为 t
给
pdf-isearch-hl-matches
,从而实现即使 isearch-mode
处于禁用状态时也显示高亮,那我们可以模仿它,注入一个变量以控制高亮的显示,如下:
1
2
3
4
5
6
7
|
(defvar-local osfva-pdf-isearch-highlight-matches nil)
(defun osfva-pdf-isearch-hl-matches-controllable-highlight (orig-fun current matches &optional occur-hack-p)
(funcall orig-fun current matches (or osfva-pdf-isearch-highlight-matches occur-hack-p)))
(advice-add #'pdf-isearch-hl-matches
:around #'osfva-pdf-isearch-hl-matches-controllable-highlight)
|
接着,我们创建自己的 isearch-repeat-forward/isearch-repeat-backward
,不同于原函数,我们的函数会设置 osfva-pdf-isearch-hl-matches-controllable-highlight
为non-nil以保证 isearch-mode
处于禁用状态时也显示高亮,如下:
1
2
3
4
5
6
7
8
9
|
(defun osfva-pdf-isearch-repeat-forward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(isearch-repeat-forward arg))
(defun osfva-pdf-isearch-repeat-backward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(isearch-repeat-backward arg))
|
最后,我们绑定这两个函数到 n/N
键,如下:
1
2
3
|
(evil-define-key* 'normal pdf-view-mode-map
(kbd "n") #'osfva-pdf-isearch-repeat-forward
(kbd "N") #'osfva-pdf-isearch-repeat-backward)
|
至此,就实现了按 n/N
保持高亮了,但是在搜索过程中,我们会发现一个问题,正常搜索超过PDF结尾时,isearch会暂停,再按一下搜索才会wrap到PDF开头重新开始搜索,这里的问题是,暂停的时候,结尾的高亮会不见,要再按一下搜索,使其wrap到PDF开头高亮才会再次正常显示,一个简单的修复方法就是改成搜索超过PDF结尾时,直接wrap到PDF开头开始搜索,不要暂停,如下:
1
2
|
(add-hook 'pdf-view-mode-hook
(lambda () (setq-local isearch-wrap-pause 'no)))
|
为了避免 osfva-pdf-isearch-hl-matches-controllable-highlight
对正常使用isearch进行搜索产生影响,我们要在pdf-tools正常使用isearch进行搜索时,重置该变量,如下:
1
2
3
4
5
|
(advice-add #'pdf-isearch-mode-initialize
:before
(lambda (&rest args)
"Reset `osfva-pdf-isearch-highlight-matches'."
(setq osfva-pdf-isearch-highlight-matches nil)))
|
针对 pdf-isearch-occur
也可以进行类似的处理,不过读者思考下,会发现
osfva-pdf-isearch-hl-matches-controllable-highlight
的具体值和 pdf-isearch-occur
间接传递 occur-hack-p
为 t
给 pdf-isearch-hl-matches
并不冲突,所以可以不处理。
除此之外,我们还得考虑清除高亮的问题,因为高亮一直都在,我们希望按某个键可以清除高亮,比如 ESC
就是一个合适的选择,先定义清除高亮的函数,如下:
1
2
3
|
(defun osfva-pdf-isearch-cleanup-highlight ()
(setq osfva-pdf-isearch-highlight-matches nil)
(pdf-view-redisplay))
|
就是调用 pdf-view-redisplay
redisplay下,高亮就清除了,还得把 osfva-pdf-isearch-highlight-matches
重置了。
接下来定义另外一个函数,该函数用于替代原始 ESC
的功能,原始 ESC
是进入evil normal state,现在我们替换成进入evil normal state的同时,清除高亮,如下:
1
2
3
4
5
|
(evil-define-command osfva-pdf-view-force-normal-state ()
:repeat abort
:suppress-operator t
(evil-normal-state)
(osfva-pdf-isearch-cleanup-highlight))
|
将该函数绑定到 ESC
上,和之前的绑定一起写,如下:
1
2
3
4
|
(evil-define-key* 'normal pdf-view-mode-map
(kbd "n") #'osfva-pdf-isearch-repeat-forward
(kbd "N") #'osfva-pdf-isearch-repeat-backward
(kbd "<escape>") #'osfva-pdf-view-force-normal-state)
|
继续解决下一个问题,即绑定到 n/N
的命令 isearch-repeat-forward/isearch-repeat-backward
不会记住上一次的搜索方向,定义我们自己的搜索函数,让它们记住上一次的搜索方向,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
(defvar-local osfva-pdf-isearch-forward? t)
(defun osfva-pdf-isearch-forward (&optional regexp-p no-recursive-edit)
"Like `isearch-forward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? t)
(isearch-forward regexp-p no-recursive-edit))
(defun osfva-pdf-isearch-backward (&optional regexp-p no-recursive-edit)
"Like `isearch-backward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? nil)
(isearch-backward regexp-p no-recursive-edit))
|
接下来,修改之前定义的 osfva-pdf-isearch-repeat-forward
以及 osfva-pdf-isearch-repeat-backward
,让它们根据记住的搜索方向,自己选择合适方向的搜索函数,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
(defun osfva-pdf-isearch-repeat-forward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-forward arg)
(isearch-repeat-backward arg)))
(defun osfva-pdf-isearch-repeat-backward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-backward arg)
(isearch-repeat-forward arg)))
|
将新的搜索函数绑定到 /
和 ?
上,和之前的绑定一起写,如下:
1
2
3
4
5
6
|
(evil-define-key* 'normal pdf-view-mode-map
(kbd "/") #'osfva-pdf-isearch-forward
(kbd "?") #'osfva-pdf-isearch-backward
(kbd "n") #'osfva-pdf-isearch-repeat-forward
(kbd "N") #'osfva-pdf-isearch-repeat-backward
(kbd "<escape>") #'osfva-pdf-view-force-normal-state)
|
至此,所有想要的功能都完成了,完整代码如下(外层得用
with-eval-after-load
包裹住,否则一些函数可能未定义,也可以放
use-package的 :config
区域):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
(with-eval-after-load 'pdf-tools
(defun osfva-isearch-failed? ()
(or (not isearch-success) isearch-error))
(defvar-local osfva-pdf-isearch-highlight-matches nil)
(defun osfva-pdf-isearch-cleanup-highlight ()
(setq osfva-pdf-isearch-highlight-matches nil)
(pdf-view-redisplay))
(defun osfva-pdf-isearch-hl-matches-controllable-highlight (orig-fun current matches &optional occur-hack-p)
(funcall orig-fun current matches (or osfva-pdf-isearch-highlight-matches occur-hack-p)))
(advice-add #'pdf-isearch-hl-matches
:around #'osfva-pdf-isearch-hl-matches-controllable-highlight)
(defvar-local osfva-pdf-isearch-forward? t)
(defun osfva-pdf-isearch-forward (&optional regexp-p no-recursive-edit)
"Like `isearch-forward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? t)
(isearch-forward regexp-p no-recursive-edit))
(defun osfva-pdf-isearch-backward (&optional regexp-p no-recursive-edit)
"Like `isearch-backward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? nil)
(isearch-backward regexp-p no-recursive-edit))
(defun osfva-pdf-isearch-repeat-forward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-forward arg)
(isearch-repeat-backward arg)))
(defun osfva-pdf-isearch-repeat-backward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-backward arg)
(isearch-repeat-forward arg)))
(add-hook 'pdf-view-mode-hook
(lambda () (setq-local isearch-wrap-pause 'no)))
(evil-define-command osfva-pdf-view-force-normal-state ()
:repeat abort
:suppress-operator t
(evil-normal-state)
(osfva-pdf-isearch-cleanup-highlight))
(advice-add #'pdf-isearch-mode-initialize
:before
(lambda (&rest args)
"Reset `osfva-pdf-isearch-highlight-matches'."
(setq osfva-pdf-isearch-highlight-matches nil)))
(defun osfva-pdf-isearch-mode-cleanup ()
"Don't cleanup highlight if search successfully."
(pdf-isearch-active-mode -1)
(when (osfva-isearch-failed?) (pdf-view-redisplay)))
(advice-add #'pdf-isearch-mode-cleanup
:override #'osfva-pdf-isearch-mode-cleanup)
(evil-define-key* 'normal pdf-view-mode-map
(kbd "/") #'osfva-pdf-isearch-forward
(kbd "?") #'osfva-pdf-isearch-backward
(kbd "n") #'osfva-pdf-isearch-repeat-forward
(kbd "N") #'osfva-pdf-isearch-repeat-backward
(kbd "<escape>") #'osfva-pdf-view-force-normal-state))
|
还有一个问题,上面的代码仅适用于没有使用evil-collection包的情况,如果使用evil-collection,还得考虑按键绑定被evil-collection覆盖的情况,我们定义一个宏来处理,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
(defmacro osfva-with-eval-after-evil-collection-mode (mode &rest body)
(declare (indent defun))
(let ((feature-name-var (gensym "feature-name"))
(mode-var (gensym "mode"))
(mode-str-var (gensym "mode-str"))
(hook-fun-name-var (gensym "hook-fun-name-var")))
`(let* ((,mode-var ,mode)
(,mode-str-var (symbol-name ,mode-var))
(,feature-name-var (intern (concat "evil-collection-" ,mode-str-var))))
(if (featurep ,feature-name-var)
;; In current implmentation of evil-collection, when the
;; file is already loaded, the setup function has also
;; called, so we can evaluate the body directly in this
;; case.
(progn ,@body)
(let ((,hook-fun-name-var
;; Intern the generated symbol, otherwise,
;; evil-collection cannot find the definition of the
;; hook function (this generally won't cause
;; conflicts because `gensym' will append a global
;; counter to the generated symbol).
(intern
(symbol-name
(gensym (concat "osfva-with-eval-after-evil-collection-mode-"
,mode-str-var))))))
(defalias ,hook-fun-name-var
(lambda (mode keymaps)
(when (eq mode ,mode-var)
,@body))
(concat "Override the things set up by the evil-collection mode " ,mode-str-var "."))
(add-hook 'evil-collection-setup-hook ,hook-fun-name-var))))))
|
它做的就是判断evil-collection在对应mode的按键绑定设置完没,如果还没有,那就安排在它设置完之后再执行body设置我们的按键绑定,如果已经设置完了,那就直接执行body设置我们的按键绑定。这里使用gensym生成hook函数名,由于生成的符号是未intern的,这会导致evil-collection无法调用我们的hook函数,于是我们需要提前intern下,同时由于gensym会在符号名后面附加一个全局计数器,加上我们的函数有前缀 osfva-
,因此一般不会产生名称冲突。
hook函数则是用 defalias
定义的,因为我们的函数名是在运行时动态生成的,而 defun
不会eval函数名参数。
使用evil-collection包对应的完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
(with-eval-after-load 'pdf-tools
(defun osfva-isearch-failed? ()
(or (not isearch-success) isearch-error))
(defvar-local osfva-pdf-isearch-highlight-matches nil)
(defun osfva-pdf-isearch-cleanup-highlight ()
(setq osfva-pdf-isearch-highlight-matches nil)
(pdf-view-redisplay))
(defun osfva-pdf-isearch-hl-matches-controllable-highlight (orig-fun current matches &optional occur-hack-p)
(funcall orig-fun current matches (or osfva-pdf-isearch-highlight-matches occur-hack-p)))
(advice-add #'pdf-isearch-hl-matches
:around #'osfva-pdf-isearch-hl-matches-controllable-highlight)
(defvar-local osfva-pdf-isearch-forward? t)
(defun osfva-pdf-isearch-forward (&optional regexp-p no-recursive-edit)
"Like `isearch-forward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? t)
(isearch-forward regexp-p no-recursive-edit))
(defun osfva-pdf-isearch-backward (&optional regexp-p no-recursive-edit)
"Like `isearch-backward', but remember the previous search direction."
(interactive "P\np")
(setq osfva-pdf-isearch-forward? nil)
(isearch-backward regexp-p no-recursive-edit))
(defun osfva-pdf-isearch-repeat-forward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-forward arg)
(isearch-repeat-backward arg)))
(defun osfva-pdf-isearch-repeat-backward (&optional arg)
(interactive "P")
(setq osfva-pdf-isearch-highlight-matches t)
(if osfva-pdf-isearch-forward?
(isearch-repeat-backward arg)
(isearch-repeat-forward arg)))
(add-hook 'pdf-view-mode-hook
(lambda () (setq-local isearch-wrap-pause 'no)))
(evil-define-command osfva-pdf-view-force-normal-state ()
:repeat abort
:suppress-operator t
(evil-normal-state)
(osfva-pdf-isearch-cleanup-highlight))
(advice-add #'pdf-isearch-mode-initialize
:before
(lambda (&rest args)
"Reset `osfva-pdf-isearch-highlight-matches'."
(setq osfva-pdf-isearch-highlight-matches nil)))
(defun osfva-pdf-isearch-mode-cleanup ()
"Don't cleanup highlight if search successfully."
(pdf-isearch-active-mode -1)
(when (osfva-isearch-failed?) (pdf-view-redisplay)))
(advice-add #'pdf-isearch-mode-cleanup
:override #'osfva-pdf-isearch-mode-cleanup)
(defmacro osfva-with-eval-after-evil-collection-mode (mode &rest body)
(declare (indent defun))
(let ((feature-name-var (gensym "feature-name"))
(mode-var (gensym "mode"))
(mode-str-var (gensym "mode-str"))
(hook-fun-name-var (gensym "hook-fun-name-var")))
`(let* ((,mode-var ,mode)
(,mode-str-var (symbol-name ,mode-var))
(,feature-name-var (intern (concat "evil-collection-" ,mode-str-var))))
(if (featurep ,feature-name-var)
;; In current implmentation of evil-collection, when the
;; file is already loaded, the setup function has also
;; called, so we can evaluate the body directly in this
;; case.
(progn ,@body)
(let ((,hook-fun-name-var
;; Intern the generated symbol, otherwise,
;; evil-collection cannot find the definition of the
;; hook function (this generally won't cause
;; conflicts because `gensym' will append a global
;; counter to the generated symbol).
(intern
(symbol-name
(gensym (concat "osfva-with-eval-after-evil-collection-mode-"
,mode-str-var))))))
(defalias ,hook-fun-name-var
(lambda (mode keymaps)
(when (eq mode ,mode-var)
,@body))
(concat "Override the things set up by the evil-collection mode " ,mode-str-var "."))
(add-hook 'evil-collection-setup-hook ,hook-fun-name-var))))))
(osfva-with-eval-after-evil-collection-mode 'pdf
(evil-define-key* 'normal pdf-view-mode-map
(kbd "/") #'osfva-pdf-isearch-forward
(kbd "?") #'osfva-pdf-isearch-backward
(kbd "n") #'osfva-pdf-isearch-repeat-forward
(kbd "N") #'osfva-pdf-isearch-repeat-backward
(kbd "<escape>") #'osfva-pdf-view-force-normal-state)))
|