branch: externals/doc-view-follow commit 592eb01b8ba102fba33f9ddee46152857126170d Author: Paul Nelson <ultr...@gmail.com> Commit: Paul Nelson <ultr...@gmail.com>
Refactor using generics * README.org (Overview): Update. (Installation): Add section for configuring pdf-tools support. (Extensibility): Revise to describe the new generic-based mechanism. * doc-view-follow.el: Replace mode config alist with generic methods. (doc-view-follow-supported-p, doc-view-follow-setup) (doc-view-follow-teardown, doc-view-follow-set-page) (doc-view-follow-get-page): New generic methods to handle mode-specific operations. New implementations for doc-view-mode. (doc-view-follow-sync-pages, doc-view-follow-mode) (global-doc-view-follow-mode): Use them. --- README.org | 13 ++++- doc-view-follow.el | 151 ++++++++++++++++++++++++++++------------------------- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/README.org b/README.org index b89c6512f0..690173456c 100644 --- a/README.org +++ b/README.org @@ -3,7 +3,7 @@ * Overview -doc-view-follow.el provides a minor mode, =doc-view-follow-mode=, that automatically synchronizes page navigation between multiple windows displaying the same PS/PDF/DVI/DjVu document. This is essentially an analogue of Emacs' built-in =follow-mode=, but for document buffers instead rather than text buffers. +doc-view-follow.el provides a minor mode, =doc-view-follow-mode=, that automatically synchronizes page navigation between multiple windows displaying the same document (PS/PDF/DVI/DjVu, etc.). This is essentially an analogue of Emacs' built-in =follow-mode=, but for document buffers instead rather than text buffers. In particular, this allows for a convenient "book view," with two windows showing consecutive pages side-by-side. @@ -16,6 +16,15 @@ Alternatively, download the source file and run: M-x package-install-file RET /path/to/doc-view-follow.el RET #+end_src +** PDF-Tools Support (Optional) + +This package supports the built-in =doc-view-mode= as well as =pdf-view-mode= from [[https://github.com/vedang/pdf-tools][pdf-tools]]. If you use pdf-tools, add the following to your init file: + +#+begin_src emacs-lisp +(with-eval-after-load 'pdf-view + (require 'doc-view-follow-pdf-tools)) +#+end_src + * Usage After installation, you can activate the mode: @@ -41,4 +50,4 @@ Both windows will automatically stay synchronized. * Extensibility -One could add support for additional document viewing modes (if any) via the =doc-view-follow-modes= variable. +The package uses generic functions, making it straightforward to add support for additional document viewing modes. See =doc-view-follow-pdf-tools.el= for an example of how to do this. diff --git a/doc-view-follow.el b/doc-view-follow.el index c2a401a730..8bf43aba4f 100644 --- a/doc-view-follow.el +++ b/doc-view-follow.el @@ -34,9 +34,15 @@ ;; particular, this allows a "book view" where the document is shown ;; in two side-by-side windows on consecutive pages. ;; -;; `doc-view-mode' (built-in to Emacs) and `pdf-view-mode' (from -;; pdf-tools) are supported by default. Additional viewing modes (if -;; any) could be added via the `doc-view-follow-modes' variable. +;; `doc-view-mode' (built-in to Emacs) is supported by default. To +;; enable support for `pdf-view-mode' (from pdf-tools), add the +;; following to your init file: +;; +;; (with-eval-after-load 'pdf-view +;; (require 'doc-view-follow-pdf-tools)) +;; +;; The file `doc-view-follow-pdf-tools.el' shows by example how the +;; package might be extended to other document viewing modes. ;; ;; Usage: ;; - Enable globally: M-x global-doc-view-follow-mode @@ -56,95 +62,96 @@ "Synchronize pages between two windows displaying the same document." :group 'convenience) -(defvar doc-view-follow-modes - '((doc-view-mode - :goto doc-view-goto-page - :next doc-view-next-page - :prev doc-view-previous-page - :current (lambda () (doc-view-current-page)) - :max (lambda () (doc-view-last-page-number))) - (pdf-view-mode - :goto pdf-view-goto-page - :next pdf-view-next-page-command - :prev pdf-view-previous-page-command - :current (lambda () (pdf-view-current-page)) - :max (lambda () (pdf-cache-number-of-pages)))) - "Alist of supported major modes and relevant functions. -Each entry has the format: (MAJOR-MODE . CONFIG), where CONFIG is a list -with entries: - -:goto GOTO-PAGE-FUNCTION -:next NEXT-PAGE-FUNCTION -:prev PREV-PAGE-FUNCTION -:current FUNCTION-RETURNING-CURRENT-PAGE -:max FUNCTION-RETURNING-MAX-PAGE - -Other packages can add support for additional document viewing modes -by adding entries to this list.") - -(defun doc-view-follow--call-func (mode-config action &rest args) - "Call function for ACTION from MODE-CONFIG with ARGS." - (apply (plist-get mode-config action) args)) +(cl-defgeneric doc-view-follow-supported-p () + "Check if the current buffer is supported by `doc-view-follow-mode'." + nil) + +(cl-defgeneric doc-view-follow-setup () + "Setup function for `doc-view-follow-mode'." + (error "`doc-view-follow-mode' is not supported in this buffer")) + +(cl-defgeneric doc-view-follow-teardown () + "Teardown function for `doc-view-follow-mode'." + (error "`doc-view-follow-mode' is not supported in this buffer")) + +(cl-defgeneric doc-view-follow-set-page (_page) + "Go to PAGE in the current document buffer. +Clamps PAGE to the valid range of pages." + (error "`doc-view-follow-mode' is not supported in this buffer")) + +(cl-defgeneric doc-view-follow-get-page () + "Return the current page number in the document buffer." + (error "`doc-view-follow-mode' is not supported in this buffer")) (defvar doc-view-follow--sync-in-progress nil "Flag to prevent recursive sync operations.") ;;;###autoload (define-minor-mode doc-view-follow-mode - "Minor mode to sync pages between two windows showing the same document." + "Minor mode to sync pages between windows showing the same document." :global nil + :lighter nil + (unless (doc-view-follow-supported-p) + (error "`doc-view-follow-mode' is not supported in this buffer")) (if doc-view-follow-mode - (doc-view-follow--manage-advice 'add) - (unless (doc-view-follow--some-buffer-active-p) - (doc-view-follow--manage-advice 'remove)))) + (progn + (doc-view-follow-setup) + (doc-view-follow-sync-pages)) + (doc-view-follow-teardown))) -(defun doc-view-follow--sync-pages (&rest _args) +(defun doc-view-follow-sync-pages (&rest _args) "Sync pages between windows showing the same document." (when (and doc-view-follow-mode (not doc-view-follow--sync-in-progress)) - (let ((doc-view-follow--sync-in-progress t)) - (when-let* - ((cfg (cdr (assoc major-mode doc-view-follow-modes))) - (windows (follow-all-followers)) - ((> (length windows) 1))) - (let* ((current-page (doc-view-follow--call-func cfg :current)) - (max-page (doc-view-follow--call-func cfg :max)) - (current-window (selected-window)) - (window-index (seq-position windows current-window)) - (i 0)) + (let ((doc-view-follow--sync-in-progress t) + (windows (follow-all-followers))) + (when (> (length windows) 1) + (let* ((current-i (seq-position windows (selected-window))) + (current-page (doc-view-follow-get-page)) + (page (- current-page current-i))) (dolist (win windows) - (let ((target-page - (min max-page - (max 1 (+ current-page (- i window-index)))))) + (unless (eq (selected-window) win) (with-selected-window win - (doc-view-follow--call-func cfg :goto target-page))) - (setq i (1+ i)))))))) - -(defun doc-view-follow--manage-advice (add-or-remove) - "Add or remove advice for all functions in `doc-view-follow-modes'. -ADD-OR-REMOVE should be either \\='add or \\='remove." - (dolist (mode-entry doc-view-follow-modes) - (dolist (action '(:goto :next :prev)) - (when-let* ((func (plist-get (cdr mode-entry) action))) - (if (eq add-or-remove 'add) - (advice-add func :after #'doc-view-follow--sync-pages) - (advice-remove func #'doc-view-follow--sync-pages)))))) - -(defun doc-view-follow--some-buffer-active-p () - "Return non-nil if some buffer has `doc-view-follow-mode' active." - (seq-some (lambda (buf) - (buffer-local-value 'doc-view-follow-mode buf)) - (buffer-list))) + (doc-view-follow-set-page page))) + (incf page))))))) (defun doc-view-follow--maybe-enable () "Enable `doc-view-follow-mode' if appropriate for this buffer." - (when (assq major-mode doc-view-follow-modes) - (doc-view-follow-mode 1))) + (when (doc-view-follow-supported-p) + (unless doc-view-follow-mode + (doc-view-follow-mode 1)))) ;;;###autoload (define-globalized-minor-mode global-doc-view-follow-mode - doc-view-follow-mode - doc-view-follow--maybe-enable) + doc-view-follow-mode doc-view-follow--maybe-enable) + +(require 'doc-view) + +(cl-defmethod doc-view-follow-supported-p (&context (major-mode doc-view-mode)) + "When MAJOR-MODE is `doc-view-mode', return t." + t) + +(cl-defmethod doc-view-follow-setup (&context (major-mode doc-view-mode)) + "When MAJOR-MODE is `doc-view-mode', setup `doc-view-follow-mode'." + (advice-add 'doc-view-goto-page :after #'doc-view-follow-sync-pages)) + +(cl-defmethod doc-view-follow-teardown (&context (major-mode doc-view-mode)) + "When MAJOR-MODE is `doc-view-mode', teardown `doc-view-follow-mode'." + (unless + (seq-some (lambda (buf) + (and (eq (buffer-local-value 'major-mode buf) 'doc-view-mode) + (buffer-local-value 'doc-view-follow-mode buf))) + (buffer-list)) + (advice-remove 'doc-view-goto-page #'doc-view-follow-sync-pages))) + +(cl-defmethod doc-view-follow-set-page (page &context (major-mode doc-view-mode)) + "When MAJOR-MODE is `doc-view-mode', go to PAGE in the document buffer." + (let ((page (max 1 (min page (doc-view-last-page-number))))) + (doc-view-goto-page page))) + +(cl-defmethod doc-view-follow-get-page (&context (major-mode doc-view-mode)) + "When MAJOR-MODE is `doc-view-mode', return the current page number." + (doc-view-current-page)) (provide 'doc-view-follow) ;;; doc-view-follow.el ends here