821 lines
30 KiB
EmacsLisp
821 lines
30 KiB
EmacsLisp
|
;;; pdf-occur.el --- Display matching lines of PDF documents. -*- lexical-binding: t -*-
|
|||
|
|
|||
|
;; Copyright (C) 2013, 2014 Andreas Politz
|
|||
|
|
|||
|
;; Author: Andreas Politz <politza@fh-trier.de>
|
|||
|
;; Keywords: files, multimedia
|
|||
|
|
|||
|
;; This program is free software; you can redistribute it and/or modify
|
|||
|
;; it under the terms of the GNU General Public License as published by
|
|||
|
;; the Free Software Foundation, either version 3 of the License, or
|
|||
|
;; (at your option) any later version.
|
|||
|
|
|||
|
;; This program is distributed in the hope that it will be useful,
|
|||
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
;; GNU General Public License for more details.
|
|||
|
|
|||
|
;; You should have received a copy of the GNU General Public License
|
|||
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
;;; Commentary:
|
|||
|
;;
|
|||
|
|
|||
|
(require 'pdf-tools)
|
|||
|
(require 'pdf-view)
|
|||
|
(require 'pdf-util)
|
|||
|
(require 'pdf-info)
|
|||
|
(require 'pdf-isearch)
|
|||
|
(require 'tablist)
|
|||
|
(require 'ibuf-ext)
|
|||
|
(require 'dired)
|
|||
|
(require 'let-alist)
|
|||
|
|
|||
|
;;; Code:
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Custom & Variables
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
(defgroup pdf-occur nil
|
|||
|
"Display matching lines of PDF documents."
|
|||
|
:group 'pdf-tools)
|
|||
|
|
|||
|
(defface pdf-occur-document-face
|
|||
|
'((default (:inherit font-lock-string-face)))
|
|||
|
"Face used to highlight documents in the list buffer.")
|
|||
|
|
|||
|
(defface pdf-occur-page-face
|
|||
|
'((default (:inherit font-lock-type-face)))
|
|||
|
"Face used to highlight page numbers in the list buffer.")
|
|||
|
|
|||
|
(defcustom pdf-occur-search-batch-size 16
|
|||
|
"Maximum number of pages searched in one query.
|
|||
|
|
|||
|
Lower numbers will make Emacs more responsive when searching at
|
|||
|
the cost of slightly increased search time."
|
|||
|
:type 'integer)
|
|||
|
|
|||
|
(defcustom pdf-occur-prefer-string-search nil
|
|||
|
"If non-nil, reverse the meaning of the regexp-p prefix-arg."
|
|||
|
:type 'boolean)
|
|||
|
|
|||
|
(defvar pdf-occur-history nil
|
|||
|
"The history variable for search strings.")
|
|||
|
|
|||
|
(defvar pdf-occur-search-pages-left nil
|
|||
|
"The total number of pages left to search.")
|
|||
|
|
|||
|
(defvar pdf-occur-search-documents nil
|
|||
|
"The list of searched documents.
|
|||
|
|
|||
|
Each element should be either the filename of a PDF document or a
|
|||
|
cons \(FILENAME . PAGES\), where PAGES is the list of pages to
|
|||
|
search. See `pdf-info-normalize-page-range' for its format.")
|
|||
|
|
|||
|
(defvar pdf-occur-number-of-matches 0
|
|||
|
"The number of matches in all searched documents.")
|
|||
|
|
|||
|
(defvar pdf-occur-search-string nil
|
|||
|
"The currently used search string, resp. regexp.")
|
|||
|
|
|||
|
(defvar pdf-occur-search-regexp-p nil
|
|||
|
"Non-nil, if searching for a regexp.")
|
|||
|
|
|||
|
(defvar pdf-occur-buffer-mode-map
|
|||
|
(let ((kmap (make-sparse-keymap)))
|
|||
|
(set-keymap-parent kmap tablist-mode-map)
|
|||
|
(define-key kmap (kbd "RET") #'pdf-occur-goto-occurrence)
|
|||
|
(define-key kmap (kbd "C-o") #'pdf-occur-view-occurrence)
|
|||
|
(define-key kmap (kbd "SPC") #'pdf-occur-view-occurrence)
|
|||
|
(define-key kmap (kbd "C-c C-f") #'next-error-follow-minor-mode)
|
|||
|
(define-key kmap (kbd "g") #'pdf-occur-revert-buffer-with-args)
|
|||
|
(define-key kmap (kbd "K") #'pdf-occur-abort-search)
|
|||
|
(define-key kmap (kbd "D") #'pdf-occur-tablist-do-delete)
|
|||
|
(define-key kmap (kbd "x") #'pdf-occur-tablist-do-flagged-delete)
|
|||
|
(define-key kmap (kbd "A") #'pdf-occur-tablist-gather-documents)
|
|||
|
kmap)
|
|||
|
"The keymap used for `pdf-occur-buffer-mode'.")
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * High level functions
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
(define-derived-mode pdf-occur-buffer-mode tablist-mode "PDFOccur"
|
|||
|
"Major mode for output from `pdf-occur`. \\<pdf-occur-buffer-mode-map>
|
|||
|
|
|||
|
Some useful keys are:
|
|||
|
|
|||
|
\\[pdf-occur-abort-search] - Abort the search.
|
|||
|
\\[pdf-occur-revert-buffer-with-args] - Restart the search.
|
|||
|
\\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Restart search with different regexp.
|
|||
|
\\[universal-argument] \\[universal-argument] \\[pdf-occur-revert-buffer-with-args] - Same, but do a plain string search.
|
|||
|
|
|||
|
\\[tablist-push-regexp-filter] - Filter matches by regexp on current or prefix-th column.
|
|||
|
\\[tablist-pop-filter] - Remove last added filter.
|
|||
|
|
|||
|
\\[pdf-occur-tablist-do-delete] - Remove the current file from the search.
|
|||
|
\\[pdf-occur-tablist-gather-documents] - Include marked files from displayed `dired'/`ibuffer' and
|
|||
|
`pdf-view-mode' buffers in the search.
|
|||
|
|
|||
|
\\{pdf-occur-buffer-mode-map}"
|
|||
|
(setq-local case-fold-search case-fold-search)
|
|||
|
(setq-local next-error-function #'pdf-occur-next-error)
|
|||
|
(setq-local revert-buffer-function
|
|||
|
#'pdf-occur-revert-buffer)
|
|||
|
(setq next-error-last-buffer (current-buffer))
|
|||
|
(setq-local tabulated-list-sort-key nil)
|
|||
|
(setq-local tabulated-list-use-header-line t)
|
|||
|
(setq-local tablist-operations-function
|
|||
|
(lambda (op &rest _)
|
|||
|
(cl-case op
|
|||
|
(supported-operations '(find-entry))
|
|||
|
(find-entry
|
|||
|
(let ((display-buffer-overriding-action
|
|||
|
'(display-buffer-same-window)))
|
|||
|
(pdf-occur-goto-occurrence)))))))
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(defun pdf-occur (string &optional regexp-p)
|
|||
|
"List lines matching STRING or PCRE.
|
|||
|
|
|||
|
Interactively search for a regexp. Unless a prefix arg was given,
|
|||
|
in which case this functions performs a string search.
|
|||
|
|
|||
|
If `pdf-occur-prefer-string-search' is non-nil, the meaning of
|
|||
|
the prefix-arg is inverted."
|
|||
|
(interactive
|
|||
|
(progn
|
|||
|
(pdf-util-assert-pdf-buffer)
|
|||
|
(list
|
|||
|
(pdf-occur-read-string
|
|||
|
(pdf-occur-want-regexp-search-p))
|
|||
|
(pdf-occur-want-regexp-search-p))))
|
|||
|
(pdf-util-assert-pdf-buffer)
|
|||
|
(pdf-occur-search (list (current-buffer)) string regexp-p))
|
|||
|
|
|||
|
(defvar ibuffer-filtering-qualifiers)
|
|||
|
;;;###autoload
|
|||
|
(defun pdf-occur-multi-command ()
|
|||
|
"Perform `pdf-occur' on multiple buffer.
|
|||
|
|
|||
|
For a programmatic search of multiple documents see
|
|||
|
`pdf-occur-search'."
|
|||
|
(interactive)
|
|||
|
(ibuffer)
|
|||
|
(with-current-buffer "*Ibuffer*"
|
|||
|
(pdf-occur-ibuffer-minor-mode)
|
|||
|
(unless (member '(derived-mode . pdf-view-mode)
|
|||
|
ibuffer-filtering-qualifiers)
|
|||
|
(ibuffer-filter-by-derived-mode 'pdf-view-mode))
|
|||
|
(message
|
|||
|
"%s"
|
|||
|
(substitute-command-keys
|
|||
|
"Mark a bunch of PDF buffers and type \\[pdf-occur-ibuffer-do-occur]"))
|
|||
|
(sit-for 3)))
|
|||
|
|
|||
|
(defun pdf-occur-revert-buffer (&rest _)
|
|||
|
"Restart the search."
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(unless pdf-occur-search-documents
|
|||
|
(error "No documents to search"))
|
|||
|
(unless pdf-occur-search-string
|
|||
|
(error "Nothing to search for"))
|
|||
|
(let* ((2-columns-p (= 1 (length pdf-occur-search-documents)))
|
|||
|
(filename-width
|
|||
|
(min 24
|
|||
|
(apply #'max
|
|||
|
(mapcar #'length
|
|||
|
(mapcar #'pdf-occur-abbrev-document
|
|||
|
(mapcar #'car pdf-occur-search-documents))))))
|
|||
|
(page-sorter (tablist-generate-sorter
|
|||
|
(if 2-columns-p 0 1)
|
|||
|
'<
|
|||
|
'string-to-number)))
|
|||
|
(setq tabulated-list-format
|
|||
|
(if 2-columns-p
|
|||
|
`[("Page" 4 ,page-sorter :right-align t)
|
|||
|
("Line" 0 t)]
|
|||
|
`[("Document" ,filename-width t)
|
|||
|
("Page" 4 ,page-sorter :right-align t)
|
|||
|
("Line" 0 t)])
|
|||
|
tabulated-list-entries nil))
|
|||
|
(tabulated-list-revert)
|
|||
|
(pdf-occur-start-search
|
|||
|
pdf-occur-search-documents
|
|||
|
pdf-occur-search-string
|
|||
|
pdf-occur-search-regexp-p)
|
|||
|
(pdf-occur-update-header-line)
|
|||
|
(setq mode-line-process
|
|||
|
'(:propertize ":run" face compilation-mode-line-run)))
|
|||
|
|
|||
|
(defun pdf-occur-revert-buffer-with-args (string &optional regexp-p documents)
|
|||
|
"Restart the search with modified arguments.
|
|||
|
|
|||
|
Interactively just restart the search, unless a prefix was given.
|
|||
|
In this case read a new search string. With `C-u C-u' as prefix
|
|||
|
additionally invert the current state of
|
|||
|
`pdf-occur-search-regexp-p'."
|
|||
|
(interactive
|
|||
|
(progn
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(cond
|
|||
|
(current-prefix-arg
|
|||
|
(let ((regexp-p
|
|||
|
(if (equal current-prefix-arg '(16))
|
|||
|
(not pdf-occur-search-regexp-p)
|
|||
|
pdf-occur-search-regexp-p)))
|
|||
|
(list
|
|||
|
(pdf-occur-read-string regexp-p)
|
|||
|
regexp-p)))
|
|||
|
(t
|
|||
|
(list pdf-occur-search-string
|
|||
|
pdf-occur-search-regexp-p)))))
|
|||
|
(setq pdf-occur-search-string string
|
|||
|
pdf-occur-search-regexp-p regexp-p)
|
|||
|
(when documents
|
|||
|
(setq pdf-occur-search-documents
|
|||
|
(pdf-occur-normalize-documents documents)))
|
|||
|
(pdf-occur-revert-buffer))
|
|||
|
|
|||
|
(defun pdf-occur-abort-search ()
|
|||
|
"Abort the current search.
|
|||
|
|
|||
|
This immediately kills the search process."
|
|||
|
(interactive)
|
|||
|
(unless (pdf-occur-search-in-progress-p)
|
|||
|
(user-error "No search in progress"))
|
|||
|
(pdf-info-kill-local-server)
|
|||
|
(pdf-occur-search-finished t))
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Finding occurrences
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
|
|||
|
(defun pdf-occur-goto-occurrence (&optional no-select-window-p)
|
|||
|
"Go to the occurrence at point.
|
|||
|
|
|||
|
If EVENT is nil, use occurrence at current line. Select the
|
|||
|
PDF's window, unless NO-SELECT-WINDOW-P is non-nil.
|
|||
|
|
|||
|
FIXME: EVENT not used at the moment."
|
|||
|
(interactive)
|
|||
|
(let ((item (tabulated-list-get-id)))
|
|||
|
(when item
|
|||
|
(let* ((doc (plist-get item :document))
|
|||
|
(page (plist-get item :page))
|
|||
|
(match (plist-get item :match-edges))
|
|||
|
(buffer (if (bufferp doc)
|
|||
|
doc
|
|||
|
(or (find-buffer-visiting doc)
|
|||
|
(find-file-noselect doc))))
|
|||
|
window)
|
|||
|
(if no-select-window-p
|
|||
|
(setq window (display-buffer buffer))
|
|||
|
(pop-to-buffer buffer)
|
|||
|
(setq window (selected-window)))
|
|||
|
(with-selected-window window
|
|||
|
(when page
|
|||
|
(pdf-view-goto-page page))
|
|||
|
;; Abuse isearch.
|
|||
|
(when match
|
|||
|
(let ((pixel-match
|
|||
|
(pdf-util-scale-relative-to-pixel match))
|
|||
|
(pdf-isearch-batch-mode t))
|
|||
|
(pdf-isearch-hl-matches pixel-match nil t)
|
|||
|
(pdf-isearch-focus-match-batch pixel-match))))))))
|
|||
|
|
|||
|
(defun pdf-occur-view-occurrence (&optional _event)
|
|||
|
"View the occurrence at EVENT.
|
|||
|
|
|||
|
If EVENT is nil, use occurrence at current line."
|
|||
|
(interactive (list last-nonmenu-event))
|
|||
|
(pdf-occur-goto-occurrence t))
|
|||
|
|
|||
|
(defun pdf-occur-next-error (&optional arg reset)
|
|||
|
"Move to the Nth (default 1) next match in an PDF Occur mode buffer.
|
|||
|
Compatibility function for \\[next-error] invocations."
|
|||
|
(interactive "p")
|
|||
|
;; we need to run pdf-occur-find-match from within the Occur buffer
|
|||
|
(with-current-buffer
|
|||
|
;; Choose the buffer and make it current.
|
|||
|
(if (next-error-buffer-p (current-buffer))
|
|||
|
(current-buffer)
|
|||
|
(next-error-find-buffer
|
|||
|
nil nil
|
|||
|
(lambda ()
|
|||
|
(eq major-mode 'pdf-occur-buffer-mode))))
|
|||
|
(when (bobp)
|
|||
|
(setq reset t))
|
|||
|
(if reset
|
|||
|
(goto-char (point-min))
|
|||
|
(beginning-of-line))
|
|||
|
(when (/= arg 0)
|
|||
|
(when (eobp)
|
|||
|
(forward-line -1))
|
|||
|
(when reset
|
|||
|
(cl-decf arg))
|
|||
|
(let ((line (line-number-at-pos))
|
|||
|
(limit (line-number-at-pos
|
|||
|
(if (>= arg 0)
|
|||
|
(1- (point-max))
|
|||
|
(point-min)))))
|
|||
|
(when (= line limit)
|
|||
|
(error "No more matches"))
|
|||
|
(forward-line
|
|||
|
(if (>= arg 0)
|
|||
|
(min arg (- limit line))
|
|||
|
(max arg (- limit line))))))
|
|||
|
;; In case the *Occur* buffer is visible in a nonselected window.
|
|||
|
(tablist-move-to-major-column)
|
|||
|
(let ((win (get-buffer-window (current-buffer) t)))
|
|||
|
(if win (set-window-point win (point))))
|
|||
|
(pdf-occur-goto-occurrence)))
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Integration with other modes
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(define-minor-mode pdf-occur-global-minor-mode
|
|||
|
"Enable integration of Pdf Occur with other modes.
|
|||
|
|
|||
|
This global minor mode enables (or disables)
|
|||
|
`pdf-occur-ibuffer-minor-mode' and `pdf-occur-dired-minor-mode'
|
|||
|
in all current and future ibuffer/dired buffer."
|
|||
|
:global t
|
|||
|
(let ((arg (if pdf-occur-global-minor-mode 1 -1)))
|
|||
|
(dolist (buf (buffer-list))
|
|||
|
(with-current-buffer buf
|
|||
|
(cond
|
|||
|
((derived-mode-p 'dired-mode)
|
|||
|
(pdf-occur-dired-minor-mode arg))
|
|||
|
((derived-mode-p 'ibuffer-mode)
|
|||
|
(pdf-occur-ibuffer-minor-mode arg)))))
|
|||
|
(cond
|
|||
|
(pdf-occur-global-minor-mode
|
|||
|
(add-hook 'dired-mode-hook #'pdf-occur-dired-minor-mode)
|
|||
|
(add-hook 'ibuffer-mode-hook #'pdf-occur-ibuffer-minor-mode))
|
|||
|
(t
|
|||
|
(remove-hook 'dired-mode-hook #'pdf-occur-dired-minor-mode)
|
|||
|
(remove-hook 'ibuffer-mode-hook #'pdf-occur-ibuffer-minor-mode)))))
|
|||
|
|
|||
|
(defvar pdf-occur-ibuffer-minor-mode-map
|
|||
|
(let ((map (make-sparse-keymap)))
|
|||
|
(define-key map [remap ibuffer-do-occur] #'pdf-occur-ibuffer-do-occur)
|
|||
|
map)
|
|||
|
"Keymap used in `pdf-occur-ibuffer-minor-mode'.")
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(define-minor-mode pdf-occur-ibuffer-minor-mode
|
|||
|
"Hack into ibuffer's do-occur binding.
|
|||
|
|
|||
|
This mode remaps `ibuffer-do-occur' to
|
|||
|
`pdf-occur-ibuffer-do-occur', which will start the PDF Tools
|
|||
|
version of `occur', if all marked buffer's are in `pdf-view-mode'
|
|||
|
and otherwise fallback to `ibuffer-do-occur'.")
|
|||
|
|
|||
|
(defun pdf-occur-ibuffer-do-occur (&optional regexp-p)
|
|||
|
"Uses `pdf-occur-search', if appropriate.
|
|||
|
|
|||
|
I.e. all marked buffers are in PDFView mode."
|
|||
|
(interactive
|
|||
|
(list (pdf-occur-want-regexp-search-p)))
|
|||
|
(let* ((buffer (or (ibuffer-get-marked-buffers)
|
|||
|
(and (ibuffer-current-buffer)
|
|||
|
(list (ibuffer-current-buffer)))))
|
|||
|
(pdf-only-p (cl-every
|
|||
|
(lambda (buf)
|
|||
|
(with-current-buffer buf
|
|||
|
(derived-mode-p 'pdf-view-mode)))
|
|||
|
buffer)))
|
|||
|
(if (not pdf-only-p)
|
|||
|
(call-interactively 'ibuffer-do-occur)
|
|||
|
(let ((regexp (pdf-occur-read-string regexp-p)))
|
|||
|
(pdf-occur-search buffer regexp regexp-p)))))
|
|||
|
|
|||
|
(defvar pdf-occur-dired-minor-mode-map
|
|||
|
(let ((map (make-sparse-keymap)))
|
|||
|
(define-key map [remap dired-do-search] #'pdf-occur-dired-do-search)
|
|||
|
map)
|
|||
|
"Keymap used in `pdf-occur-dired-minor-mode'.")
|
|||
|
|
|||
|
;;;###autoload
|
|||
|
(define-minor-mode pdf-occur-dired-minor-mode
|
|||
|
"Hack into dired's `dired-do-search' binding.
|
|||
|
|
|||
|
This mode remaps `dired-do-search' to
|
|||
|
`pdf-occur-dired-do-search', which will start the PDF Tools
|
|||
|
version of `occur', if all marked buffer's are in `pdf-view-mode'
|
|||
|
and otherwise fallback to `dired-do-search'.")
|
|||
|
|
|||
|
(defun pdf-occur-dired-do-search ()
|
|||
|
"Uses `pdf-occur-search', if appropriate.
|
|||
|
|
|||
|
I.e. all marked files look like PDF documents."
|
|||
|
(interactive)
|
|||
|
(let ((files (dired-get-marked-files)))
|
|||
|
(if (not (cl-every (lambda (file)
|
|||
|
(string-match-p
|
|||
|
(car pdf-tools-auto-mode-alist-entry)
|
|||
|
file))
|
|||
|
files))
|
|||
|
(call-interactively 'dired-do-search)
|
|||
|
(let* ((regex-p (pdf-occur-want-regexp-search-p))
|
|||
|
(regexp (pdf-occur-read-string regex-p)))
|
|||
|
(pdf-occur-search files regexp regex-p)))))
|
|||
|
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Search engine
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
|
|||
|
(defun pdf-occur-search (documents string &optional regexp-p)
|
|||
|
"Search DOCUMENTS for STRING.
|
|||
|
|
|||
|
DOCUMENTS should be a list of buffers (objects, not names),
|
|||
|
filenames or conses \(BUFFER-OR-FILENAME . PAGES\), where PAGES
|
|||
|
determines the scope of the search of the respective document.
|
|||
|
See `pdf-info-normalize-page-range' for its format.
|
|||
|
|
|||
|
STRING is either the string to search for or, if REGEXP-P is
|
|||
|
non-nil, a Perl compatible regular expression (PCRE).
|
|||
|
|
|||
|
Display the occur buffer and start the search asynchronously.
|
|||
|
|
|||
|
Returns the window where the buffer is displayed."
|
|||
|
|
|||
|
(unless documents
|
|||
|
(error "No documents to search"))
|
|||
|
(when (or (null string) (= (length string) 0))
|
|||
|
(error "Not searching for the empty string"))
|
|||
|
(with-current-buffer (get-buffer-create "*PDF-Occur*")
|
|||
|
(pdf-occur-buffer-mode)
|
|||
|
(setq-local pdf-occur-search-documents
|
|||
|
(pdf-occur-normalize-documents documents))
|
|||
|
(setq-local pdf-occur-search-string string)
|
|||
|
(setq-local pdf-occur-search-regexp-p regexp-p)
|
|||
|
(setq-local pdf-occur-search-pages-left 0)
|
|||
|
(setq-local pdf-occur-number-of-matches 0)
|
|||
|
(pdf-occur-revert-buffer)
|
|||
|
(display-buffer
|
|||
|
(current-buffer))))
|
|||
|
|
|||
|
(advice-add 'tabulated-list-init-header :after #'pdf-occur--update-header)
|
|||
|
(defun pdf-occur--update-header (&rest _)
|
|||
|
"We want our own headers, thank you."
|
|||
|
(when (derived-mode-p 'pdf-occur-buffer-mode)
|
|||
|
(save-current-buffer
|
|||
|
(with-no-warnings (pdf-occur-update-header-line)))))
|
|||
|
|
|||
|
(defun pdf-occur-create-entry (filename page &optional match)
|
|||
|
"Create a `tabulated-list-entries' entry for a search result.
|
|||
|
|
|||
|
If match is nil, create a fake entry for documents w/o any
|
|||
|
matches linked with PAGE."
|
|||
|
(let* ((text (or (car match) "[No matches]"))
|
|||
|
(edges (cdr match))
|
|||
|
(displayed-text
|
|||
|
(if match
|
|||
|
(replace-regexp-in-string "\n" "\\n" text t t)
|
|||
|
(propertize text 'face 'font-lock-warning-face)))
|
|||
|
(displayed-page
|
|||
|
(if match
|
|||
|
(propertize (format "%d" page)
|
|||
|
'face 'pdf-occur-page-face)
|
|||
|
""))
|
|||
|
(displayed-document
|
|||
|
(propertize
|
|||
|
(pdf-occur-abbrev-document filename)
|
|||
|
'face 'pdf-occur-document-face))
|
|||
|
(id `(:document ,filename
|
|||
|
:page ,page
|
|||
|
:match-text ,(if match text)
|
|||
|
:match-edges ,(if match edges))))
|
|||
|
(list id
|
|||
|
(if (= (length pdf-occur-search-documents) 1)
|
|||
|
(vector displayed-page displayed-text)
|
|||
|
(vector displayed-document
|
|||
|
displayed-page
|
|||
|
displayed-text)))))
|
|||
|
|
|||
|
(defun pdf-occur-update-header-line ()
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(save-current-buffer
|
|||
|
;;force-mode-line-update seems to sometimes spuriously change the
|
|||
|
;;current buffer.
|
|||
|
(setq header-line-format
|
|||
|
`(:eval (concat
|
|||
|
(if (= (length pdf-occur-search-documents) 1)
|
|||
|
(format "%d match%s in document `%s'"
|
|||
|
pdf-occur-number-of-matches
|
|||
|
(if (/= 1 pdf-occur-number-of-matches) "es" "")
|
|||
|
(pdf-occur-abbrev-document
|
|||
|
(caar pdf-occur-search-documents)))
|
|||
|
(format "%d match%s in %d documents"
|
|||
|
pdf-occur-number-of-matches
|
|||
|
(if (/= 1 pdf-occur-number-of-matches) "es" "")
|
|||
|
(length pdf-occur-search-documents)))
|
|||
|
(if (pdf-occur-search-in-progress-p)
|
|||
|
(propertize
|
|||
|
(concat " ["
|
|||
|
(if (numberp pdf-occur-search-pages-left)
|
|||
|
(format "%d pages left"
|
|||
|
pdf-occur-search-pages-left)
|
|||
|
"Searching")
|
|||
|
"]")
|
|||
|
'face 'compilation-mode-line-run)))))
|
|||
|
(force-mode-line-update)))
|
|||
|
|
|||
|
(defun pdf-occur-search-finished (&optional abort-p)
|
|||
|
(setq pdf-occur-search-pages-left 0)
|
|||
|
(setq mode-line-process
|
|||
|
(if abort-p
|
|||
|
'(:propertize
|
|||
|
":aborted" face compilation-mode-line-fail)
|
|||
|
'(:propertize
|
|||
|
":exit" face compilation-mode-line-exit)))
|
|||
|
(let ((unmatched
|
|||
|
(mapcar (lambda (doc)
|
|||
|
(pdf-occur-create-entry doc 1))
|
|||
|
(cl-set-difference
|
|||
|
(mapcar #'car
|
|||
|
pdf-occur-search-documents)
|
|||
|
(mapcar (lambda (elt)
|
|||
|
(plist-get (car elt) :document))
|
|||
|
tabulated-list-entries)
|
|||
|
:test 'equal))))
|
|||
|
(when (and unmatched
|
|||
|
(> (length pdf-occur-search-documents) 1))
|
|||
|
(pdf-occur-insert-entries unmatched)))
|
|||
|
(tablist-apply-filter)
|
|||
|
(pdf-occur-update-header-line)
|
|||
|
(pdf-isearch-message
|
|||
|
(if abort-p
|
|||
|
"Search aborted."
|
|||
|
(format "Occur search finished with %d matches"
|
|||
|
pdf-occur-number-of-matches))))
|
|||
|
|
|||
|
(defun pdf-occur-add-matches (filename matches)
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(when matches
|
|||
|
(let (entries)
|
|||
|
(dolist (match matches)
|
|||
|
(let-alist match
|
|||
|
(push (pdf-occur-create-entry filename .page (cons .text .edges))
|
|||
|
entries)))
|
|||
|
(setq entries (nreverse entries))
|
|||
|
(pdf-occur-insert-entries entries))))
|
|||
|
|
|||
|
(defun pdf-occur-insert-entries (entries)
|
|||
|
"Insert tabulated-list ENTRIES at the end."
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(let ((inhibit-read-only t)
|
|||
|
(end-of-buffer (and (eobp) (not (bobp)))))
|
|||
|
(save-excursion
|
|||
|
(goto-char (point-max))
|
|||
|
(dolist (elt entries)
|
|||
|
(apply tabulated-list-printer elt))
|
|||
|
(set-buffer-modified-p nil))
|
|||
|
(when end-of-buffer
|
|||
|
(dolist (win (get-buffer-window-list))
|
|||
|
(set-window-point win (point-max))))
|
|||
|
(setq tabulated-list-entries
|
|||
|
(append tabulated-list-entries
|
|||
|
entries))))
|
|||
|
|
|||
|
(defun pdf-occur-search-in-progress-p ()
|
|||
|
(and (numberp pdf-occur-search-pages-left)
|
|||
|
(> pdf-occur-search-pages-left 0)))
|
|||
|
|
|||
|
(defun pdf-occur-start-search (documents string
|
|||
|
&optional regexp-p)
|
|||
|
(pdf-occur-assert-occur-buffer-p)
|
|||
|
(pdf-info-make-local-server nil t)
|
|||
|
(let ((batches (pdf-occur-create-batches
|
|||
|
documents (or pdf-occur-search-batch-size 1))))
|
|||
|
(pdf-info-local-batch-query
|
|||
|
(lambda (document pages)
|
|||
|
(if regexp-p
|
|||
|
(pdf-info-search-regexp string pages nil document)
|
|||
|
(pdf-info-search-string string pages document)))
|
|||
|
(lambda (status response document pages)
|
|||
|
(if status
|
|||
|
(error "%s" response)
|
|||
|
(when (numberp pdf-occur-search-pages-left)
|
|||
|
(cl-decf pdf-occur-search-pages-left
|
|||
|
(1+ (- (cdr pages) (car pages)))))
|
|||
|
(when (cl-member document pdf-occur-search-documents
|
|||
|
:key 'car
|
|||
|
:test 'equal)
|
|||
|
(cl-incf pdf-occur-number-of-matches
|
|||
|
(length response))
|
|||
|
(pdf-occur-add-matches document response)
|
|||
|
(pdf-occur-update-header-line))))
|
|||
|
(lambda (status buffer)
|
|||
|
(when (buffer-live-p buffer)
|
|||
|
(with-current-buffer buffer
|
|||
|
(pdf-occur-search-finished (eq status 'killed)))))
|
|||
|
batches)
|
|||
|
(setq pdf-occur-number-of-matches 0)
|
|||
|
(setq pdf-occur-search-pages-left
|
|||
|
(apply #'+ (mapcar (lambda (elt)
|
|||
|
(1+ (- (cdr (nth 1 elt))
|
|||
|
(car (nth 1 elt)))))
|
|||
|
batches)))))
|
|||
|
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Editing searched documents
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
(defun pdf-occur-tablist-do-delete (&optional arg)
|
|||
|
"Delete ARG documents from the search list."
|
|||
|
(interactive "P")
|
|||
|
(when (pdf-occur-search-in-progress-p)
|
|||
|
(user-error "Can't delete while a search is in progress."))
|
|||
|
(let* ((items (tablist-get-marked-items arg))
|
|||
|
(documents (cl-remove-duplicates
|
|||
|
(mapcar (lambda (entry)
|
|||
|
(plist-get (car entry) :document))
|
|||
|
items)
|
|||
|
:test 'equal)))
|
|||
|
(unless documents
|
|||
|
(error "No documents selected"))
|
|||
|
(when (tablist-yes-or-no-p
|
|||
|
'Stop\ searching
|
|||
|
nil (mapcar (lambda (d) (cons nil (vector d)))
|
|||
|
documents))
|
|||
|
(setq pdf-occur-search-documents
|
|||
|
(cl-remove-if (lambda (elt)
|
|||
|
(member (car elt) documents))
|
|||
|
pdf-occur-search-documents)
|
|||
|
tabulated-list-entries
|
|||
|
(cl-remove-if (lambda (elt)
|
|||
|
(when (member (plist-get (car elt) :document)
|
|||
|
documents)
|
|||
|
(when (plist-get (car elt) :match-edges)
|
|||
|
(cl-decf pdf-occur-number-of-matches))
|
|||
|
t))
|
|||
|
tabulated-list-entries))
|
|||
|
(tablist-revert)
|
|||
|
(pdf-occur-update-header-line)
|
|||
|
(tablist-move-to-major-column))))
|
|||
|
|
|||
|
(defun pdf-occur-tablist-do-flagged-delete (&optional interactive)
|
|||
|
"Stop searching all documents marked with a D."
|
|||
|
(interactive "p")
|
|||
|
(let* ((tablist-marker-char ?D))
|
|||
|
(if (save-excursion
|
|||
|
(goto-char (point-min))
|
|||
|
(re-search-forward (tablist-marker-regexp) nil t))
|
|||
|
(pdf-occur-tablist-do-delete)
|
|||
|
(or (not interactive)
|
|||
|
(message "(No deletions requested)")))))
|
|||
|
|
|||
|
(defun pdf-occur-tablist-gather-documents ()
|
|||
|
"Gather marked documents in windows.
|
|||
|
|
|||
|
Examine all dired/ibuffer windows and offer to put marked files
|
|||
|
in the search list."
|
|||
|
(interactive)
|
|||
|
(let ((searched (mapcar #'car pdf-occur-search-documents))
|
|||
|
files)
|
|||
|
(dolist (win (window-list))
|
|||
|
(with-selected-window win
|
|||
|
(cond
|
|||
|
((derived-mode-p 'dired-mode)
|
|||
|
(let ((marked (dired-get-marked-files nil nil nil t)))
|
|||
|
(when (> (length marked) 1)
|
|||
|
(when (eq t (car marked))
|
|||
|
(setq marked (cdr marked)))
|
|||
|
(setq files
|
|||
|
(append files marked nil)))))
|
|||
|
((derived-mode-p 'ibuffer-mode)
|
|||
|
(dolist (fname (mapcar #'buffer-file-name
|
|||
|
(ibuffer-get-marked-buffers)))
|
|||
|
(when fname
|
|||
|
(push fname files))))
|
|||
|
((and (derived-mode-p 'pdf-view-mode)
|
|||
|
(buffer-file-name))
|
|||
|
(push (buffer-file-name) files)))))
|
|||
|
|
|||
|
(setq files
|
|||
|
(cl-sort ;Looks funny.
|
|||
|
(cl-set-difference
|
|||
|
(cl-remove-duplicates
|
|||
|
(cl-remove-if-not
|
|||
|
(lambda (file) (string-match-p
|
|||
|
(car pdf-tools-auto-mode-alist-entry)
|
|||
|
file))
|
|||
|
files)
|
|||
|
:test 'file-equal-p)
|
|||
|
searched
|
|||
|
:test 'file-equal-p)
|
|||
|
'string-lessp))
|
|||
|
(if (null files)
|
|||
|
(message "No marked, new PDF files found in windows")
|
|||
|
(when (tablist-yes-or-no-p
|
|||
|
'add nil (mapcar (lambda (file)
|
|||
|
(cons nil (vector file)))
|
|||
|
(cl-sort files #'string-lessp)))
|
|||
|
(setq pdf-occur-search-documents
|
|||
|
(append pdf-occur-search-documents
|
|||
|
(pdf-occur-normalize-documents files)))
|
|||
|
(message "Added %d file%s to the list of searched documents%s"
|
|||
|
(length files)
|
|||
|
(dired-plural-s (length files))
|
|||
|
(substitute-command-keys
|
|||
|
" - Hit \\[pdf-occur-revert-buffer-with-args]"))))))
|
|||
|
|
|||
|
|
|||
|
;; * ================================================================== *
|
|||
|
;; * Utilities
|
|||
|
;; * ================================================================== *
|
|||
|
|
|||
|
(defun pdf-occur-read-string (&optional regexp-p)
|
|||
|
(read-string
|
|||
|
(concat
|
|||
|
(format "List lines %s"
|
|||
|
(if regexp-p "matching PCRE" "containing string"))
|
|||
|
(if pdf-occur-search-string
|
|||
|
(format " (default %s)" pdf-occur-search-string))
|
|||
|
": ")
|
|||
|
nil 'pdf-occur-history pdf-occur-search-string))
|
|||
|
|
|||
|
(defun pdf-occur-assert-occur-buffer-p ()
|
|||
|
(unless (derived-mode-p 'pdf-occur-buffer-mode)
|
|||
|
(error "Not in PDF occur buffer")))
|
|||
|
|
|||
|
(defun pdf-occur-want-regexp-search-p ()
|
|||
|
(or (and current-prefix-arg
|
|||
|
pdf-occur-prefer-string-search)
|
|||
|
(and (null current-prefix-arg)
|
|||
|
(not pdf-occur-prefer-string-search))))
|
|||
|
|
|||
|
;; FIXME: This will be confusing when searching documents with the
|
|||
|
;; same base file-name.
|
|||
|
(defun pdf-occur-abbrev-document (file-or-buffer)
|
|||
|
(if (bufferp file-or-buffer)
|
|||
|
(buffer-name file-or-buffer)
|
|||
|
(let ((abbrev (file-name-nondirectory file-or-buffer)))
|
|||
|
(if (> (length abbrev) 0)
|
|||
|
abbrev
|
|||
|
file-or-buffer))))
|
|||
|
|
|||
|
(defun pdf-occur-create-batches (documents batch-size)
|
|||
|
(let (queries)
|
|||
|
(dolist (d documents)
|
|||
|
(let* ((file-or-buffer (car d))
|
|||
|
(pages (pdf-info-normalize-page-range (cdr d)))
|
|||
|
(first (car pages))
|
|||
|
(last (if (eq (cdr pages) 0)
|
|||
|
(pdf-info-number-of-pages file-or-buffer)
|
|||
|
(cdr pages)))
|
|||
|
(npages (1+ (- last first)))
|
|||
|
(nbatches (ceiling
|
|||
|
(/ (float npages) batch-size))))
|
|||
|
(dotimes (i nbatches)
|
|||
|
(push
|
|||
|
(list file-or-buffer
|
|||
|
(cons (+ first (* i batch-size))
|
|||
|
(min last (+ first (1- (* (1+ i) batch-size))))))
|
|||
|
queries))))
|
|||
|
(nreverse queries)))
|
|||
|
|
|||
|
(defun pdf-occur-normalize-documents (documents)
|
|||
|
"Normalize list of documents.
|
|||
|
|
|||
|
Replaces buffers with their associated filenames \(if
|
|||
|
applicable\) and ensures that every element looks like
|
|||
|
\(FILENAME-OR-BUFFER . PAGES\)."
|
|||
|
(cl-sort (mapcar (lambda (doc)
|
|||
|
(unless (consp doc)
|
|||
|
(setq doc (cons doc nil)))
|
|||
|
(when (and (bufferp (car doc))
|
|||
|
(buffer-file-name (car doc)))
|
|||
|
(setq doc (cons (buffer-file-name (car doc))
|
|||
|
(cdr doc))))
|
|||
|
(if (stringp (car doc))
|
|||
|
(cons (expand-file-name (car doc)) (cdr doc))
|
|||
|
doc))
|
|||
|
documents)
|
|||
|
(lambda (a b) (string-lessp
|
|||
|
(if (bufferp a) (buffer-name a) a)
|
|||
|
(if (bufferp b) (buffer-name b) b)))
|
|||
|
:key 'car))
|
|||
|
|
|||
|
(provide 'pdf-occur)
|
|||
|
|
|||
|
;;; pdf-occur.el ends here
|