diff --git a/README.org b/README.org index c710806..a5c3b80 100644 --- a/README.org +++ b/README.org @@ -4,37 +4,77 @@ #+html: MELPA devdocs.el is a documentation viewer for Emacs similar to the built-in -Info browser, but geared towards documentation obtained from the -[[https://devdocs.io][DevDocs]] website. The stable version is available from [[https://elpa.gnu.org/packages/devdocs.html][GNU ELPA]] and a +Info browser, but geared towards documentation distributed by the +[[https://devdocs.io][DevDocs]] website. Currently, this covers over 500 versions of 188 +different software components. + +The stable version of the package is available from [[https://elpa.gnu.org/packages/devdocs.html][GNU ELPA]] and a development version is available from [[https://melpa.org/#/devdocs][MELPA]]; to install, type =M-x package-install RET devdocs=. #+caption: image [[https://user-images.githubusercontent.com/6500902/135726213-683b1f7d-5502-4afa-a549-c1aedaad8519.png]] +** Basic usage + To get started, download some documentation with =M-x -devdocs-install=. This will first query https://devdocs.io for the -available documents and save the selected one to disk. Once you have -the desired documents at hand, call =M-x devdocs-lookup= to search for -entries. +devdocs-install=. This will query https://devdocs.io for the +available documents and save the selected one to disk. To read the +installed documentation, there are two options: + +- =devdocs-peruse=: Select a document and display its first page. +- =devdocs-lookup=: Select an index entry and display it. + +It's handy to have a keybinding for the latter command. One +possibility, in analogy to =C-h S= (=info-lookup-symbol=), is + +#+begin_src elisp + (global-set-key (kbd ("C-h D")) 'devdocs-lookup) +#+end_src In any given buffer, the first call to =devdocs-lookup= will query for a list of documents to search (you can select more than one option by entering a comma-separated list). This selection will be remembered in subsequent calls to =devdocs-lookup=, unless a prefix argument is given; in this case you can select a new list of documents. -Alternatively, you can set the =devdocs-current-docs= variable -directly, say via [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][dir-local variables]] or a mode hook: + +In the =*devdocs*= buffer, navigation keys similar to Info and +=*Help*= buffers are available; press =C-h m= for details. Internal +hyperlinks are opened in the same viewing buffer, and external links +are opened as =browse-url= normally would. + +** Managing documents + +To manage the collection of installed documents, use the following +commands: + +- =devdocs-install=: Download and install (or reinstall) a document + distributed by [[https://devdocs.io]]. +- =devdocs-delete=: Remove an installed document. +- =devdocs-update-all=: Download and reinstall all installed documents + for which a newer version is available. + +In some cases, variants of a document are available for each (major) +version. It is possible to install several versions in parallel. + +Documents are installed under =devdocs-data-dir=, which defaults to +=~/.emacs.d/devdocs=. To completely uninstall the package, remove +this directory. + +** Setting the default documents for a collection of buffers + +You may wish to select a predefined list of documents in all buffers +of a certain major mode or project. To achieve this, set the +=devdocs-current-docs= variable directly, say via [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html][dir-local variables]] +or a mode hook: #+begin_src elisp (add-hook 'python-mode-hook (lambda () (setq-local devdocs-current-docs '("python~3.9")))) #+end_src -In the =*devdocs*= buffer, navigation keys similar to Info and -=*Help*= buffers are available; press =C-h m= for details. Internal -hyperlinks are opened in the same viewing buffer, and external links -are opened as =browse-url= normally would. +As usual, calling =devdocs-lookup= with a prefix argument redefines +the selected documents for that specific buffer. ** Contributing diff --git a/devdocs.el b/devdocs.el index 767b8a7..73f0a64 100644 --- a/devdocs.el +++ b/devdocs.el @@ -90,6 +90,16 @@ name and a count." Fontification is done using the `org-src' library, which see." :type 'boolean) +(defcustom devdocs-isearch-wrap #'devdocs-isearch-wrap + "How to wrap isearch when reaching the beginning or end of a page. +This is used as `isearch-wrap-function' in DevDocs buffers. If +nil, keep the default wrapping behavior." + :type '(choice (const :tag "Jump to next or previous page" #'devdocs-isearch-wrap) + (const :tag "Wrap as usual, staying in the same page" nil) + function)) + +(defvar devdocs-buffer-name "*devdocs*") + (defvar devdocs-history nil "History of documentation entries.") @@ -117,7 +127,7 @@ its return value; take the necessary precautions." (timer-set-time (car data) (time-add nil devdocs-cache-timeout))) (let ((val (funcall fun)) (timer (run-at-time devdocs-cache-timeout nil - (lambda () (remhash funrep devdocs--cache))))) + #'remhash funrep devdocs--cache))) (prog1 val (puthash funrep (cons timer val) devdocs--cache))))) @@ -169,11 +179,11 @@ otherwise, offer only installed documents. Return a document metadata alist if MULTIPLE is nil; otherwise, a list of metadata alists." - (let ((cands (seq-map (lambda (it) (cons (alist-get 'slug it) it)) - (if available - (devdocs--available-docs) - (or (devdocs--installed-docs) - (user-error "No documents in `%s'" devdocs-data-dir)))))) + (let ((cands (mapcar (lambda (it) (cons (alist-get 'slug it) it)) + (if available + (devdocs--available-docs) + (or (devdocs--installed-docs) + (user-error "No documents in `%s'" devdocs-data-dir)))))) (if multiple (delq nil (mapcar (lambda (s) (cdr (assoc s cands))) (completing-read-multiple prompt cands))) @@ -202,8 +212,8 @@ DOC is a document metadata alist." pages) (with-temp-buffer (url-insert-file-contents (format "%s/%s/db.json?%s" devdocs-cdn-url slug mtime)) - (seq-doseq (entry (let ((json-key-type 'string)) - (json-read))) + (dolist (entry (let ((json-key-type 'string)) + (json-read))) (with-temp-file (expand-file-name (url-hexify-string (format "%s.html" (car entry))) temp) (push (car entry) pages) @@ -211,8 +221,8 @@ DOC is a document metadata alist." (with-temp-buffer (url-insert-file-contents (format "%s/%s/index.json?%s" devdocs-cdn-url slug mtime)) (let ((index (json-read))) + (push `(pages . ,(vconcat (nreverse pages))) index) (with-temp-file (expand-file-name "index" temp) - (push `(pages . ,(apply #'vector (nreverse pages))) index) (prin1 index (current-buffer))))) (with-temp-file (expand-file-name "metadata" temp) (prin1 (cons devdocs--data-format-version doc) (current-buffer))) @@ -284,7 +294,9 @@ This is an alist containing `entries', `pages' and `types'." buffer-undo-list t header-line-format devdocs-header-line revert-buffer-function 'devdocs--revert-buffer - truncate-lines t)) + truncate-lines t) + (when devdocs-isearch-wrap + (setq-local isearch-wrap-function devdocs-isearch-wrap))) (defun devdocs-goto-target () "Go to the original position in a DevDocs buffer." @@ -316,10 +328,12 @@ Note that this refers to the index order, which may not coincide with the order of appearance in the text." (interactive "p") (let-alist (car devdocs--stack) + (unless .index + (user-error "No current entry")) (devdocs--render (or (ignore-error 'args-out-of-range - (seq-elt (alist-get 'entries (devdocs--index .doc)) - (+ count .index))) + (elt (alist-get 'entries (devdocs--index .doc)) + (+ count .index))) (user-error (if (< count 0) "No previous entry" "No next entry")))))) (defun devdocs-previous-entry (count) @@ -331,9 +345,10 @@ with the order of appearance in the text." "Go forward COUNT pages in this document." (interactive "p") (let-alist (car devdocs--stack) - (let* ((pages (alist-get 'pages (devdocs--index .doc))) + (let* ((pages (devdocs--with-cache + (alist-get 'pages (devdocs--index .doc)))) (page (+ count (seq-position pages (devdocs--path-file .path)))) - (path (or (ignore-error 'args-out-of-range (seq-elt pages page)) + (path (or (ignore-error 'args-out-of-range (elt pages page)) (user-error (if (< count 0) "No previous page" "No next page"))))) (devdocs--render `((doc . ,.doc) (path . ,path) @@ -359,6 +374,29 @@ with the order of appearance in the text." (kill-new url) (message "Copied %s" url)))) +(defun devdocs-isearch-wrap () + "Continue isearch in the next or previous document page." + (let ((reporter (make-progress-reporter "Searching")) + (entry (car devdocs--stack)) + (direction (if isearch-forward +1 -1))) + (with-temp-buffer + (let ((devdocs-buffer-name (buffer-name (current-buffer))) + (case-fold-search isearch-case-fold-search) + (isearch-forward t)) + (setq-local devdocs--stack (list entry)) + (while (progn + (progress-reporter-update reporter) + (when (bound-and-true-p isearch-mb-mode) + (let ((inhibit-redisplay nil)) (redisplay)) + (when quit-flag (signal 'quit nil))) + (devdocs-next-page direction) + (not (isearch-search-string isearch-string nil t)))) + (setq entry (car devdocs--stack)))) + (progress-reporter-done reporter) + (devdocs--render entry) + (setq isearch-wrapped nil) + (goto-char (if isearch-forward (point-min) (point-max))))) + (let ((map devdocs-mode-map)) (define-key map [tab] 'forward-button) (define-key map [backtab] 'backward-button) @@ -407,7 +445,7 @@ with the order of appearance in the text." ENTRY is an alist like those in the variable `devdocs--index', possibly with an additional ENTRY.fragment which overrides the fragment part of ENTRY.path." - (with-current-buffer (get-buffer-create "*devdocs*") + (with-current-buffer (get-buffer-create devdocs-buffer-name) (unless (eq major-mode 'devdocs-mode) (devdocs-mode)) (let-alist entry