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