dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

pdf-occur.el (30447B)


      1 ;;; pdf-occur.el --- Display matching lines of PDF documents. -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2013, 2014  Andreas Politz
      4 
      5 ;; Author: Andreas Politz <politza@fh-trier.de>
      6 ;; Keywords: files, multimedia
      7 
      8 ;; This program is free software; you can redistribute it and/or modify
      9 ;; it under the terms of the GNU General Public License as published by
     10 ;; the Free Software Foundation, either version 3 of the License, or
     11 ;; (at your option) any later version.
     12 
     13 ;; This program is distributed in the hope that it will be useful,
     14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16 ;; GNU General Public License for more details.
     17 
     18 ;; You should have received a copy of the GNU General Public License
     19 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     20 
     21 ;;; Commentary:
     22 ;;
     23 
     24 (require 'pdf-tools)
     25 (require 'pdf-view)
     26 (require 'pdf-util)
     27 (require 'pdf-info)
     28 (require 'pdf-isearch)
     29 (require 'tablist)
     30 (require 'ibuf-ext)
     31 (require 'dired)
     32 (require 'let-alist)
     33 
     34 ;;; Code:
     35 
     36 
     37 
     38 
     39 ;; * ================================================================== *
     40 ;; * Custom & Variables
     41 ;; * ================================================================== *
     42 
     43 (defgroup pdf-occur nil
     44   "Display matching lines of PDF documents."
     45   :group 'pdf-tools)
     46 
     47 (defface pdf-occur-document-face
     48   '((default (:inherit font-lock-string-face)))
     49   "Face used to highlight documents in the list buffer."
     50   :group 'pdf-occur)
     51 
     52 (defface pdf-occur-page-face
     53   '((default (:inherit font-lock-type-face)))
     54   "Face used to highlight page numbers in the list buffer."
     55   :group 'pdf-occur)
     56 
     57 (defcustom pdf-occur-search-batch-size 16
     58   "Maximum number of pages searched in one query.
     59 
     60 Lower numbers will make Emacs more responsive when searching at
     61 the cost of slightly increased search time."
     62   :group 'pdf-occur
     63   :type 'integer)
     64 
     65 (defcustom pdf-occur-prefer-string-search nil
     66   "If non-nil, reverse the meaning of the regexp-p prefix-arg."
     67   :group 'pdf-occur
     68   :type 'boolean)
     69 
     70 (defvar pdf-occur-history nil
     71   "The history variable for search strings.")
     72 
     73 (defvar pdf-occur-search-pages-left nil
     74   "The total number of pages left to search.")
     75 
     76 (defvar pdf-occur-search-documents nil
     77   "The list of searched documents.
     78 
     79 Each element should be either the filename of a PDF document or a
     80 cons \(FILENAME . PAGES\), where PAGES is the list of pages to
     81 search.  See `pdf-info-normalize-page-range' for it's format.")
     82 
     83 (defvar pdf-occur-number-of-matches 0
     84   "The number of matches in all searched documents.")
     85 
     86 (defvar pdf-occur-search-string nil
     87   "The currently used search string, resp. regexp.")
     88 
     89 (defvar pdf-occur-search-regexp-p nil
     90   "Non-nil, if searching for a regexp.")
     91 
     92 (defvar pdf-occur-buffer-mode-map
     93   (let ((kmap (make-sparse-keymap)))
     94     (set-keymap-parent kmap tablist-mode-map)
     95     (define-key kmap (kbd "RET") 'pdf-occur-goto-occurrence)
     96     (define-key kmap (kbd "C-o") 'pdf-occur-view-occurrence)
     97     (define-key kmap (kbd "SPC") 'pdf-occur-view-occurrence)
     98     (define-key kmap (kbd "C-c C-f") 'next-error-follow-minor-mode)
     99     (define-key kmap (kbd "g") 'pdf-occur-revert-buffer-with-args)
    100     (define-key kmap (kbd "K") 'pdf-occur-abort-search)
    101     (define-key kmap (kbd "D") 'pdf-occur-tablist-do-delete)
    102     (define-key kmap (kbd "x") 'pdf-occur-tablist-do-flagged-delete)
    103     (define-key kmap (kbd "A") 'pdf-occur-tablist-gather-documents)
    104     kmap)
    105   "The keymap used for `pdf-occur-buffer-mode'.")
    106 
    107 
    108 ;; * ================================================================== *
    109 ;; * High level functions
    110 ;; * ================================================================== *
    111 
    112 (define-derived-mode pdf-occur-buffer-mode tablist-mode "PDFOccur"
    113   "Major mode for output from `pdf-occur`. \\<pdf-occur-buffer-mode-map>
    114 
    115 Some useful keys are:
    116 
    117 \\[pdf-occur-abort-search] - Abort the search.
    118 \\[pdf-occur-revert-buffer-with-args] - Restart the search.
    119 \\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Restart search with different regexp.
    120 \\[universal-argument] \\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Same, but do a plain string search.
    121 
    122 \\[tablist-push-regexp-filter] - Filter matches by regexp on current or prefix-th column.
    123 \\[tablist-pop-filter] - Remove last added filter.
    124 
    125 \\[pdf-occur-tablist-do-delete] - Remove the current file from the search.
    126 \\[pdf-occur-tablist-gather-documents] - Include marked files from displayed `dired'/`ibuffer' and
    127     `pdf-view-mode' buffers in the search.
    128 
    129 \\{pdf-occur-buffer-mode-map}"
    130   (setq-local case-fold-search case-fold-search)
    131   (setq-local next-error-function 'pdf-occur-next-error)
    132   (setq-local revert-buffer-function
    133               'pdf-occur-revert-buffer)
    134   (setq next-error-last-buffer (current-buffer))
    135   (setq-local tabulated-list-sort-key nil)
    136   (setq-local tabulated-list-use-header-line t)
    137   (setq-local tablist-operations-function
    138               (lambda (op &rest _)
    139                 (cl-case op
    140                   (supported-operations '(find-entry))
    141                   (find-entry
    142                    (let ((display-buffer-overriding-action
    143                           '(display-buffer-same-window)))
    144                      (pdf-occur-goto-occurrence)))))))
    145 
    146 ;;;###autoload
    147 (defun pdf-occur (string &optional regexp-p)
    148   "List lines matching STRING or PCRE.
    149 
    150 Interactively search for a regexp. Unless a prefix arg was given,
    151 in which case this functions performs a string search.
    152 
    153 If `pdf-occur-prefer-string-search' is non-nil, the meaning of
    154 the prefix-arg is inverted."
    155   (interactive
    156    (progn
    157      (pdf-util-assert-pdf-buffer)
    158      (list
    159       (pdf-occur-read-string
    160        (pdf-occur-want-regexp-search-p))
    161       (pdf-occur-want-regexp-search-p))))
    162   (pdf-util-assert-pdf-buffer)
    163   (pdf-occur-search (list (current-buffer)) string regexp-p))
    164 
    165 (defvar ibuffer-filtering-qualifiers)
    166 ;;;###autoload
    167 (defun pdf-occur-multi-command ()
    168   "Perform `pdf-occur' on multiple buffer.
    169 
    170 For a programmatic search of multiple documents see
    171 `pdf-occur-search'."
    172   (interactive)
    173   (ibuffer)
    174   (with-current-buffer "*Ibuffer*"
    175     (pdf-occur-ibuffer-minor-mode)
    176     (unless (member '(derived-mode . pdf-view-mode)
    177                     ibuffer-filtering-qualifiers)
    178       (ibuffer-filter-by-derived-mode 'pdf-view-mode))
    179     (message
    180      "%s"
    181      (substitute-command-keys
    182       "Mark a bunch of PDF buffers and type \\[pdf-occur-ibuffer-do-occur]"))
    183     (sit-for 3)))
    184 
    185 (defun pdf-occur-revert-buffer (&rest _)
    186   "Restart the search."
    187   (pdf-occur-assert-occur-buffer-p)
    188   (unless pdf-occur-search-documents
    189     (error "No documents to search"))
    190   (unless pdf-occur-search-string
    191     (error "Nothing to search for"))
    192   (let* ((2-columns-p (= 1 (length pdf-occur-search-documents)))
    193          (filename-width
    194           (min 24
    195                (apply 'max
    196                  (mapcar 'length
    197                          (mapcar 'pdf-occur-abbrev-document
    198                                  (mapcar 'car pdf-occur-search-documents))))))
    199          (page-sorter (tablist-generate-sorter
    200                        (if 2-columns-p 0 1)
    201                        '<
    202                        'string-to-number)))
    203     (setq tabulated-list-format
    204           (if 2-columns-p
    205               `[("Page" 4 ,page-sorter :right-align t)
    206                 ("Line" 0 t)]
    207             `[("Document" ,filename-width t)
    208               ("Page" 4 ,page-sorter :right-align t)
    209               ("Line" 0 t)])
    210           tabulated-list-entries nil))
    211   (tabulated-list-revert)
    212   (pdf-occur-start-search
    213    pdf-occur-search-documents
    214    pdf-occur-search-string
    215    pdf-occur-search-regexp-p)
    216   (pdf-occur-update-header-line)
    217   (setq mode-line-process
    218         '(:propertize ":run" face compilation-mode-line-run)))
    219 
    220 (defun pdf-occur-revert-buffer-with-args (string &optional regexp-p documents)
    221   "Restart the search with modified arguments.
    222 
    223 Interactively just restart the search, unless a prefix was given.
    224 In this case read a new search string.  With `C-u C-u' as prefix
    225 additionally invert the current state of
    226 `pdf-occur-search-regexp-p'."
    227   (interactive
    228    (progn
    229      (pdf-occur-assert-occur-buffer-p)
    230      (cond
    231       (current-prefix-arg
    232        (let ((regexp-p
    233               (if (equal current-prefix-arg '(16))
    234                   (not pdf-occur-search-regexp-p)
    235                 pdf-occur-search-regexp-p)))
    236          (list
    237           (pdf-occur-read-string regexp-p)
    238           regexp-p)))
    239       (t
    240        (list pdf-occur-search-string
    241              pdf-occur-search-regexp-p)))))
    242   (setq pdf-occur-search-string string
    243         pdf-occur-search-regexp-p regexp-p)
    244   (when documents
    245     (setq pdf-occur-search-documents
    246           (pdf-occur-normalize-documents documents)))
    247   (pdf-occur-revert-buffer))
    248 
    249 (defun pdf-occur-abort-search ()
    250   "Abort the current search.
    251 
    252 This immediately kills the search process."
    253   (interactive)
    254   (unless (pdf-occur-search-in-progress-p)
    255     (user-error "No search in progress"))
    256   (pdf-info-kill-local-server)
    257   (pdf-occur-search-finished t))
    258 
    259 
    260 ;; * ================================================================== *
    261 ;; * Finding occurrences
    262 ;; * ================================================================== *
    263 
    264 
    265 (defun pdf-occur-goto-occurrence (&optional no-select-window-p)
    266   "Go to the occurrence at point.
    267 
    268 If EVENT is nil, use occurrence at current line.  Select the
    269 PDF's window, unless NO-SELECT-WINDOW-P is non-nil.
    270 
    271 FIXME: EVENT not used at the moment."
    272   (interactive)
    273   (let ((item (tabulated-list-get-id)))
    274     (when item
    275       (let* ((doc (plist-get item :document))
    276              (page (plist-get item :page))
    277              (match (plist-get item :match-edges))
    278              (buffer (if (bufferp doc)
    279                          doc
    280                        (or (find-buffer-visiting doc)
    281                            (find-file-noselect doc))))
    282              window)
    283         (if no-select-window-p
    284             (setq window (display-buffer buffer))
    285           (pop-to-buffer buffer)
    286           (setq window (selected-window)))
    287         (with-selected-window window
    288           (when page
    289             (pdf-view-goto-page page))
    290           ;; Abuse isearch.
    291           (when match
    292             (let ((pixel-match
    293                    (pdf-util-scale-relative-to-pixel match))
    294                   (pdf-isearch-batch-mode t))
    295               (pdf-isearch-hl-matches pixel-match nil t)
    296               (pdf-isearch-focus-match-batch pixel-match))))))))
    297 
    298 (defun pdf-occur-view-occurrence (&optional _event)
    299   "View the occurrence at EVENT.
    300 
    301 If EVENT is nil, use occurrence at current line."
    302   (interactive (list last-nonmenu-event))
    303   (pdf-occur-goto-occurrence t))
    304 
    305 (defun pdf-occur-next-error (&optional arg reset)
    306   "Move to the Nth (default 1) next match in an PDF Occur mode buffer.
    307 Compatibility function for \\[next-error] invocations."
    308   (interactive "p")
    309   ;; we need to run pdf-occur-find-match from within the Occur buffer
    310   (with-current-buffer
    311       ;; Choose the buffer and make it current.
    312       (if (next-error-buffer-p (current-buffer))
    313           (current-buffer)
    314         (next-error-find-buffer
    315          nil nil
    316          (lambda ()
    317            (eq major-mode 'pdf-occur-buffer-mode))))
    318     (when (bobp)
    319       (setq reset t))
    320     (if reset
    321         (goto-char (point-min))
    322       (beginning-of-line))
    323     (when (/= arg 0)
    324       (when (eobp)
    325         (forward-line -1))
    326       (when reset
    327         (cl-decf arg))
    328       (let ((line (line-number-at-pos))
    329             (limit (line-number-at-pos
    330                     (if (>= arg 0)
    331                         (1- (point-max))
    332                       (point-min)))))
    333         (when (= line limit)
    334           (error "No more matches"))
    335         (forward-line
    336          (if (>= arg 0)
    337              (min arg (- limit line))
    338            (max arg (- limit line))))))
    339     ;; In case the *Occur* buffer is visible in a nonselected window.
    340     (tablist-move-to-major-column)
    341     (let ((win (get-buffer-window (current-buffer) t)))
    342       (if win (set-window-point win (point))))
    343     (pdf-occur-goto-occurrence)))
    344 
    345 
    346 ;; * ================================================================== *
    347 ;; * Integration with other modes
    348 ;; * ================================================================== *
    349 
    350 ;;;###autoload
    351 (define-minor-mode pdf-occur-global-minor-mode
    352   "Enable integration of Pdf Occur with other modes.
    353 
    354 This global minor mode enables (or disables)
    355 `pdf-occur-ibuffer-minor-mode' and `pdf-occur-dired-minor-mode'
    356 in all current and future ibuffer/dired buffer."
    357   :global t
    358   :group 'pdf-occur
    359   (let ((arg (if pdf-occur-global-minor-mode 1 -1)))
    360     (dolist (buf (buffer-list))
    361       (with-current-buffer buf
    362         (cond
    363          ((derived-mode-p 'dired-mode)
    364           (pdf-occur-dired-minor-mode arg))
    365          ((derived-mode-p 'ibuffer-mode)
    366           (pdf-occur-ibuffer-minor-mode arg)))))
    367     (cond
    368      (pdf-occur-global-minor-mode
    369       (add-hook 'dired-mode-hook 'pdf-occur-dired-minor-mode)
    370       (add-hook 'ibuffer-mode-hook 'pdf-occur-ibuffer-minor-mode))
    371      (t
    372       (remove-hook 'dired-mode-hook 'pdf-occur-dired-minor-mode)
    373       (remove-hook 'ibuffer-mode-hook 'pdf-occur-ibuffer-minor-mode)))))
    374 
    375 (defvar pdf-occur-ibuffer-minor-mode-map
    376   (let ((map (make-sparse-keymap)))
    377     (define-key map [remap ibuffer-do-occur] 'pdf-occur-ibuffer-do-occur)
    378     map)
    379   "Keymap used in `pdf-occur-ibuffer-minor-mode'.")
    380 
    381 ;;;###autoload
    382 (define-minor-mode pdf-occur-ibuffer-minor-mode
    383   "Hack into ibuffer's do-occur binding.
    384 
    385 This mode remaps `ibuffer-do-occur' to
    386 `pdf-occur-ibuffer-do-occur', which will start the PDF Tools
    387 version of `occur', if all marked buffer's are in `pdf-view-mode'
    388 and otherwise fallback to `ibuffer-do-occur'."
    389   :group 'pdf-occur)
    390 
    391 (defun pdf-occur-ibuffer-do-occur (&optional regexp-p)
    392   "Uses `pdf-occur-search', if appropriate.
    393 
    394 I.e. all marked buffers are in PDFView mode."
    395   (interactive
    396    (list (pdf-occur-want-regexp-search-p)))
    397   (let* ((buffer (or (ibuffer-get-marked-buffers)
    398                      (and (ibuffer-current-buffer)
    399                           (list (ibuffer-current-buffer)))))
    400          (pdf-only-p (cl-every
    401                       (lambda (buf)
    402                         (with-current-buffer buf
    403                           (derived-mode-p 'pdf-view-mode)))
    404                       buffer)))
    405     (if (not pdf-only-p)
    406         (call-interactively 'ibuffer-do-occur)
    407       (let ((regexp (pdf-occur-read-string regexp-p)))
    408         (pdf-occur-search buffer regexp regexp-p)))))
    409 
    410 (defvar pdf-occur-dired-minor-mode-map
    411   (let ((map (make-sparse-keymap)))
    412     (define-key map [remap dired-do-search] 'pdf-occur-dired-do-search)
    413     map)
    414   "Keymap used in `pdf-occur-dired-minor-mode'.")
    415 
    416 ;;;###autoload
    417 (define-minor-mode pdf-occur-dired-minor-mode
    418   "Hack into dired's `dired-do-search' binding.
    419 
    420 This mode remaps `dired-do-search' to
    421 `pdf-occur-dired-do-search', which will start the PDF Tools
    422 version of `occur', if all marked buffer's are in `pdf-view-mode'
    423 and otherwise fallback to `dired-do-search'."
    424   :group 'pdf-occur)
    425 
    426 (defun pdf-occur-dired-do-search ()
    427   "Uses `pdf-occur-search', if appropriate.
    428 
    429 I.e. all marked files look like PDF documents."
    430   (interactive)
    431   (let ((files (dired-get-marked-files)))
    432     (if (not (cl-every (lambda (file)
    433                          (string-match-p
    434                           (car pdf-tools-auto-mode-alist-entry)
    435                           file))
    436                        files))
    437         (call-interactively 'dired-do-search)
    438       (let* ((regex-p (pdf-occur-want-regexp-search-p))
    439              (regexp (pdf-occur-read-string regex-p)))
    440         (pdf-occur-search files regexp regex-p)))))
    441 
    442 
    443 
    444 ;; * ================================================================== *
    445 ;; * Search engine
    446 ;; * ================================================================== *
    447 
    448 
    449 (defun pdf-occur-search (documents string &optional regexp-p)
    450   "Search DOCUMENTS for STRING.
    451 
    452 DOCUMENTS should be a list of buffers (objects, not names),
    453 filenames or conses \(BUFFER-OR-FILENAME . PAGES\), where PAGES
    454 determines the scope of the search of the respective document.
    455 See `pdf-info-normalize-page-range' for it's format.
    456 
    457 STRING is either the string to search for or, if REGEXP-P is
    458 non-nil, a Perl compatible regular expression (PCRE).
    459 
    460 Display the occur buffer and start the search asynchronously.
    461 
    462 Returns the window where the buffer is displayed."
    463 
    464   (unless documents
    465     (error "No documents to search"))
    466   (when (or (null string) (= (length string) 0))
    467     (error "Not searching for the empty string"))
    468   (with-current-buffer (get-buffer-create "*PDF-Occur*")
    469     (pdf-occur-buffer-mode)
    470     (setq-local pdf-occur-search-documents
    471                 (pdf-occur-normalize-documents documents))
    472     (setq-local pdf-occur-search-string string)
    473     (setq-local pdf-occur-search-regexp-p regexp-p)
    474     (setq-local pdf-occur-search-pages-left 0)
    475     (setq-local pdf-occur-number-of-matches 0)
    476     (pdf-occur-revert-buffer)
    477     (display-buffer
    478      (current-buffer))))
    479 
    480 (defadvice tabulated-list-init-header (after update-header activate)
    481   "We want our own headers, thank you."
    482   (when (derived-mode-p 'pdf-occur-buffer-mode)
    483     (save-current-buffer
    484       (with-no-warnings (pdf-occur-update-header-line)))))
    485 
    486 (defun pdf-occur-create-entry (filename page &optional match)
    487   "Create a `tabulated-list-entries' entry for a search result.
    488 
    489 If match is nil, create a fake entry for documents w/o any
    490 matches linked with PAGE."
    491   (let* ((text (or (car match) "[No matches]"))
    492          (edges (cdr match))
    493          (displayed-text
    494           (if match
    495               (replace-regexp-in-string "\n" "\\n" text t t)
    496             (propertize text 'face 'font-lock-warning-face)))
    497          (displayed-page
    498           (if match
    499               (propertize (format "%d" page)
    500                           'face 'pdf-occur-page-face)
    501             ""))
    502          (displayed-document
    503           (propertize
    504            (pdf-occur-abbrev-document filename)
    505            'face 'pdf-occur-document-face))
    506          (id `(:document ,filename
    507                :page ,page
    508                :match-text ,(if match text)
    509                :match-edges ,(if match edges))))
    510     (list id
    511           (if (= (length pdf-occur-search-documents) 1)
    512               (vector displayed-page displayed-text)
    513             (vector displayed-document
    514                     displayed-page
    515                     displayed-text)))))
    516 
    517 (defun pdf-occur-update-header-line ()
    518   (pdf-occur-assert-occur-buffer-p)
    519   (save-current-buffer
    520     ;;force-mode-line-update seems to sometimes spuriously change the
    521     ;;current buffer.
    522     (setq header-line-format
    523           `(:eval (concat
    524                    (if (= (length pdf-occur-search-documents) 1)
    525                        (format "%d match%s in document `%s'"
    526                                pdf-occur-number-of-matches
    527                                (if (/= 1 pdf-occur-number-of-matches) "es" "")
    528                                (pdf-occur-abbrev-document
    529                                 (caar pdf-occur-search-documents)))
    530                      (format "%d match%s in %d documents"
    531                              pdf-occur-number-of-matches
    532                              (if (/= 1 pdf-occur-number-of-matches) "es" "")
    533                              (length pdf-occur-search-documents)))
    534                    (if (pdf-occur-search-in-progress-p)
    535                        (propertize
    536                         (concat " ["
    537                                 (if (numberp pdf-occur-search-pages-left)
    538                                     (format "%d pages left"
    539                                             pdf-occur-search-pages-left)
    540                                   "Searching")
    541                                 "]")
    542                         'face 'compilation-mode-line-run)))))
    543     (force-mode-line-update)))
    544 
    545 (defun pdf-occur-search-finished (&optional abort-p)
    546   (setq pdf-occur-search-pages-left 0)
    547   (setq mode-line-process
    548         (if abort-p
    549             '(:propertize
    550               ":aborted" face compilation-mode-line-fail)
    551           '(:propertize
    552             ":exit" face compilation-mode-line-exit)))
    553   (let ((unmatched
    554          (mapcar (lambda (doc)
    555                    (pdf-occur-create-entry doc 1))
    556                  (cl-set-difference
    557                   (mapcar 'car
    558                           pdf-occur-search-documents)
    559                   (mapcar (lambda (elt)
    560                             (plist-get (car elt) :document))
    561                           tabulated-list-entries)
    562                   :test 'equal))))
    563     (when (and unmatched
    564                (> (length pdf-occur-search-documents) 1))
    565       (pdf-occur-insert-entries unmatched)))
    566   (tablist-apply-filter)
    567   (pdf-occur-update-header-line)
    568   (pdf-isearch-message
    569    (if abort-p
    570        "Search aborted."
    571      (format "Occur search finished with %d matches"
    572              pdf-occur-number-of-matches))))
    573 
    574 (defun pdf-occur-add-matches (filename matches)
    575   (pdf-occur-assert-occur-buffer-p)
    576   (when matches
    577     (let (entries)
    578       (dolist (match matches)
    579         (let-alist match
    580           (push (pdf-occur-create-entry filename .page (cons .text .edges))
    581                 entries)))
    582       (setq entries (nreverse entries))
    583       (pdf-occur-insert-entries entries))))
    584 
    585 (defun pdf-occur-insert-entries (entries)
    586   "Insert tabulated-list ENTRIES at the end."
    587   (pdf-occur-assert-occur-buffer-p)
    588   (let ((inhibit-read-only t)
    589         (end-of-buffer (and (eobp) (not (bobp)))))
    590     (save-excursion
    591       (goto-char (point-max))
    592       (dolist (elt entries)
    593         (apply tabulated-list-printer elt))
    594       (set-buffer-modified-p nil))
    595     (when end-of-buffer
    596       (dolist (win (get-buffer-window-list))
    597         (set-window-point win (point-max))))
    598     (setq tabulated-list-entries
    599           (append tabulated-list-entries
    600                   entries))))
    601 
    602 (defun pdf-occur-search-in-progress-p ()
    603   (and (numberp pdf-occur-search-pages-left)
    604        (> pdf-occur-search-pages-left 0)))
    605 
    606 (defun pdf-occur-start-search (documents string
    607                                          &optional regexp-p)
    608   (pdf-occur-assert-occur-buffer-p)
    609   (pdf-info-make-local-server nil t)
    610   (let ((batches (pdf-occur-create-batches
    611                   documents (or pdf-occur-search-batch-size 1))))
    612     (pdf-info-local-batch-query
    613      (lambda (document pages)
    614        (if regexp-p
    615            (pdf-info-search-regexp string pages nil document)
    616          (pdf-info-search-string string pages document)))
    617      (lambda (status response document pages)
    618        (if status
    619            (error "%s" response)
    620          (when (numberp pdf-occur-search-pages-left)
    621            (cl-decf pdf-occur-search-pages-left
    622                     (1+ (- (cdr pages) (car pages)))))
    623          (when (cl-member document pdf-occur-search-documents
    624                           :key 'car
    625                           :test 'equal)
    626            (cl-incf pdf-occur-number-of-matches
    627                     (length response))
    628            (pdf-occur-add-matches document response)
    629            (pdf-occur-update-header-line))))
    630      (lambda (status buffer)
    631        (when (buffer-live-p buffer)
    632          (with-current-buffer buffer
    633            (pdf-occur-search-finished (eq status 'killed)))))
    634      batches)
    635     (setq pdf-occur-number-of-matches 0)
    636     (setq pdf-occur-search-pages-left
    637           (apply '+ (mapcar (lambda (elt)
    638                               (1+ (- (cdr (nth 1 elt))
    639                                      (car (nth 1 elt)))))
    640                             batches)))))
    641 
    642 
    643 
    644 ;; * ================================================================== *
    645 ;; * Editing searched documents
    646 ;; * ================================================================== *
    647 
    648 (defun pdf-occur-tablist-do-delete (&optional arg)
    649   "Delete ARG documents from the search list."
    650   (interactive "P")
    651   (when (pdf-occur-search-in-progress-p)
    652     (user-error "Can't delete while a search is in progress."))
    653   (let* ((items (tablist-get-marked-items arg))
    654          (documents (cl-remove-duplicates
    655                      (mapcar (lambda (entry)
    656                                (plist-get (car entry) :document))
    657                              items)
    658                      :test 'equal)))
    659     (unless documents
    660       (error "No documents selected"))
    661     (when (tablist-yes-or-no-p
    662            'Stop\ searching
    663            nil (mapcar (lambda (d) (cons nil (vector d)))
    664                        documents))
    665       (setq pdf-occur-search-documents
    666             (cl-remove-if (lambda (elt)
    667                             (member (car elt) documents))
    668                           pdf-occur-search-documents)
    669             tabulated-list-entries
    670             (cl-remove-if (lambda (elt)
    671                             (when (member (plist-get (car elt) :document)
    672                                           documents)
    673                               (when (plist-get (car elt) :match-edges)
    674                                 (cl-decf pdf-occur-number-of-matches))
    675                               t))
    676                           tabulated-list-entries))
    677       (tablist-revert)
    678       (pdf-occur-update-header-line)
    679       (tablist-move-to-major-column))))
    680 
    681 (defun pdf-occur-tablist-do-flagged-delete (&optional interactive)
    682   "Stop searching all documents marked with a D."
    683   (interactive "p")
    684   (let* ((tablist-marker-char ?D))
    685     (if (save-excursion
    686           (goto-char (point-min))
    687           (re-search-forward (tablist-marker-regexp) nil t))
    688         (pdf-occur-tablist-do-delete)
    689       (or (not interactive)
    690           (message "(No deletions requested)")))))
    691 
    692 (defun pdf-occur-tablist-gather-documents ()
    693   "Gather marked documents in windows.
    694 
    695 Examine all dired/ibuffer windows and offer to put marked files
    696 in the search list."
    697   (interactive)
    698   (let ((searched (mapcar 'car pdf-occur-search-documents))
    699         files)
    700     (dolist (win (window-list))
    701       (with-selected-window win
    702         (cond
    703          ((derived-mode-p 'dired-mode)
    704           (let ((marked (dired-get-marked-files nil nil nil t)))
    705             (when (> (length marked) 1)
    706               (when (eq t (car marked))
    707                 (setq marked (cdr marked)))
    708               (setq files
    709                     (append files marked nil)))))
    710          ((derived-mode-p 'ibuffer-mode)
    711           (dolist (fname (mapcar 'buffer-file-name
    712                                  (ibuffer-get-marked-buffers)))
    713             (when fname
    714               (push fname files))))
    715          ((and (derived-mode-p 'pdf-view-mode)
    716                (buffer-file-name))
    717           (push (buffer-file-name) files)))))
    718 
    719     (setq files
    720           (cl-sort                      ;Looks funny.
    721            (cl-set-difference
    722             (cl-remove-duplicates
    723              (cl-remove-if-not
    724               (lambda (file) (string-match-p
    725                               (car pdf-tools-auto-mode-alist-entry)
    726                               file))
    727               files)
    728              :test 'file-equal-p)
    729             searched
    730             :test 'file-equal-p)
    731            'string-lessp))
    732     (if (null files)
    733         (message "No marked, new PDF files found in windows")
    734       (when (tablist-yes-or-no-p
    735              'add nil (mapcar (lambda (file)
    736                                 (cons nil (vector file)))
    737                               (cl-sort files 'string-lessp)))
    738         (setq pdf-occur-search-documents
    739               (append pdf-occur-search-documents
    740                       (pdf-occur-normalize-documents files)))
    741         (message "Added %d file%s to the list of searched documents%s"
    742                  (length files)
    743                  (dired-plural-s (length files))
    744                  (substitute-command-keys
    745                   " - Hit \\[pdf-occur-revert-buffer-with-args]"))))))
    746 
    747 
    748 ;; * ================================================================== *
    749 ;; * Utilities
    750 ;; * ================================================================== *
    751 
    752 (defun pdf-occur-read-string (&optional regexp-p)
    753   (read-string
    754    (concat
    755     (format "List lines %s"
    756             (if regexp-p "matching PCRE" "containing string"))
    757     (if pdf-occur-search-string
    758         (format " (default %s)" pdf-occur-search-string))
    759     ": ")
    760    nil 'pdf-occur-history pdf-occur-search-string))
    761 
    762 (defun pdf-occur-assert-occur-buffer-p ()
    763   (unless (derived-mode-p 'pdf-occur-buffer-mode)
    764     (error "Not in PDF occur buffer")))
    765 
    766 (defun pdf-occur-want-regexp-search-p ()
    767   (or (and current-prefix-arg
    768            pdf-occur-prefer-string-search)
    769       (and (null current-prefix-arg)
    770            (not pdf-occur-prefer-string-search))))
    771 
    772 ;; FIXME: This will be confusing when searching documents with the
    773 ;; same base file-name.
    774 (defun pdf-occur-abbrev-document (file-or-buffer)
    775   (if (bufferp file-or-buffer)
    776       (buffer-name file-or-buffer)
    777     (let ((abbrev (file-name-nondirectory file-or-buffer)))
    778       (if (> (length abbrev) 0)
    779           abbrev
    780         file-or-buffer))))
    781 
    782 (defun pdf-occur-create-batches (documents batch-size)
    783   (let (queries)
    784     (dolist (d documents)
    785       (let* ((file-or-buffer (car d))
    786              (pages (pdf-info-normalize-page-range (cdr d)))
    787              (first (car pages))
    788              (last (if (eq (cdr pages) 0)
    789                        (pdf-info-number-of-pages file-or-buffer)
    790                      (cdr pages)))
    791              (npages (1+ (- last first)))
    792              (nbatches (ceiling
    793                         (/ (float npages) batch-size))))
    794         (dotimes (i nbatches)
    795           (push
    796            (list file-or-buffer
    797                  (cons (+ first (* i batch-size))
    798                        (min last (+ first (1- (* (1+ i) batch-size))))))
    799            queries))))
    800     (nreverse queries)))
    801 
    802 (defun pdf-occur-normalize-documents (documents)
    803   "Normalize list of documents.
    804 
    805 Replaces buffers with their associated filenames \(if
    806 applicable\) and ensures that every element looks like
    807 \(FILENAME-OR-BUFFER . PAGES\)."
    808   (cl-sort (mapcar (lambda (doc)
    809                      (unless (consp doc)
    810                        (setq doc (cons doc nil)))
    811                      (when (and (bufferp (car doc))
    812                                 (buffer-file-name (car doc)))
    813                        (setq doc (cons (buffer-file-name (car doc))
    814                                        (cdr doc))))
    815                      (if (stringp (car doc))
    816                          (cons (expand-file-name (car doc)) (cdr doc))
    817                        doc))
    818                    documents)
    819            (lambda (a b) (string-lessp
    820                           (if (bufferp a) (buffer-name a) a)
    821                           (if (bufferp b) (buffer-name b) b)))
    822            :key 'car))
    823 
    824 (provide 'pdf-occur)
    825 
    826 ;;; pdf-occur.el ends here