config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

pdf-occur.el (30379B)


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