;;; cider-doc.el --- CIDER documentation functionality -*- lexical-binding: t -*-
|
|
|
|
;; Copyright © 2014-2019 Bozhidar Batsov, Jeff Valk and CIDER contributors
|
|
|
|
;; Author: Jeff Valk <jv@jeffvalk.com>
|
|
|
|
;; 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/>.
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
;;; Commentary:
|
|
|
|
;; Mode for formatting and presenting documentation
|
|
|
|
;;; Code:
|
|
|
|
(require 'cider-common)
|
|
(require 'subr-x)
|
|
(require 'cider-compat)
|
|
(require 'cider-util)
|
|
(require 'cider-popup)
|
|
(require 'cider-client)
|
|
(require 'cider-grimoire)
|
|
(require 'nrepl-dict)
|
|
(require 'org-table)
|
|
(require 'button)
|
|
(require 'easymenu)
|
|
(require 'cider-browse-spec)
|
|
|
|
|
|
;;; Variables
|
|
|
|
(defgroup cider-doc nil
|
|
"Documentation for CIDER."
|
|
:prefix "cider-doc-"
|
|
:group 'cider)
|
|
|
|
(defcustom cider-doc-auto-select-buffer t
|
|
"Controls whether to auto-select the doc popup buffer."
|
|
:type 'boolean
|
|
:group 'cider-doc
|
|
:package-version '(cider . "0.15.0"))
|
|
|
|
(declare-function cider-apropos "cider-apropos")
|
|
(declare-function cider-apropos-select "cider-apropos")
|
|
(declare-function cider-apropos-documentation "cider-apropos")
|
|
(declare-function cider-apropos-documentation-select "cider-apropos")
|
|
|
|
(defvar cider-doc-map
|
|
(let (cider-doc-map)
|
|
(define-prefix-command 'cider-doc-map)
|
|
(define-key cider-doc-map (kbd "a") #'cider-apropos)
|
|
(define-key cider-doc-map (kbd "C-a") #'cider-apropos)
|
|
(define-key cider-doc-map (kbd "s") #'cider-apropos-select)
|
|
(define-key cider-doc-map (kbd "C-s") #'cider-apropos-select)
|
|
(define-key cider-doc-map (kbd "f") #'cider-apropos-documentation)
|
|
(define-key cider-doc-map (kbd "C-f") #'cider-apropos-documentation)
|
|
(define-key cider-doc-map (kbd "e") #'cider-apropos-documentation-select)
|
|
(define-key cider-doc-map (kbd "C-e") #'cider-apropos-documentation-select)
|
|
(define-key cider-doc-map (kbd "d") #'cider-doc)
|
|
(define-key cider-doc-map (kbd "C-d") #'cider-doc)
|
|
(define-key cider-doc-map (kbd "r") #'cider-grimoire)
|
|
(define-key cider-doc-map (kbd "C-r") #'cider-grimoire)
|
|
(define-key cider-doc-map (kbd "w") #'cider-grimoire-web)
|
|
(define-key cider-doc-map (kbd "C-w") #'cider-grimoire-web)
|
|
(define-key cider-doc-map (kbd "j") #'cider-javadoc)
|
|
(define-key cider-doc-map (kbd "C-j") #'cider-javadoc)
|
|
cider-doc-map)
|
|
"CIDER documentation keymap.")
|
|
|
|
(defconst cider-doc-menu
|
|
'("Documentation"
|
|
["CiderDoc" cider-doc]
|
|
["JavaDoc in browser" cider-javadoc]
|
|
["Grimoire" cider-grimoire]
|
|
["Grimoire in browser" cider-grimoire-web]
|
|
["Search symbols" cider-apropos]
|
|
["Search symbols & select" cider-apropos-select]
|
|
["Search documentation" cider-apropos-documentation]
|
|
["Search documentation & select" cider-apropos-documentation-select]
|
|
"--"
|
|
["Configure Doc buffer" (customize-group 'cider-docview-mode)])
|
|
"CIDER documentation submenu.")
|
|
|
|
|
|
;;; cider-docview-mode
|
|
|
|
(defgroup cider-docview-mode nil
|
|
"Formatting/fontifying documentation viewer."
|
|
:prefix "cider-docview-"
|
|
:group 'cider)
|
|
|
|
(defcustom cider-docview-fill-column fill-column
|
|
"Fill column for docstrings in doc buffer."
|
|
:type 'list
|
|
:group 'cider-docview-mode
|
|
:package-version '(cider . "0.7.0"))
|
|
|
|
|
|
;; Faces
|
|
|
|
(defface cider-docview-emphasis-face
|
|
'((t (:inherit default :underline t)))
|
|
"Face for emphasized text"
|
|
:group 'cider-docview-mode
|
|
:package-version '(cider . "0.7.0"))
|
|
|
|
(defface cider-docview-strong-face
|
|
'((t (:inherit default :underline t :weight bold)))
|
|
"Face for strongly emphasized text"
|
|
:group 'cider-docview-mode
|
|
:package-version '(cider . "0.7.0"))
|
|
|
|
(defface cider-docview-literal-face
|
|
'((t (:inherit font-lock-string-face)))
|
|
"Face for literal text"
|
|
:group 'cider-docview-mode
|
|
:package-version '(cider . "0.7.0"))
|
|
|
|
(defface cider-docview-table-border-face
|
|
'((t (:inherit shadow)))
|
|
"Face for table borders"
|
|
:group 'cider-docview-mode
|
|
:package-version '(cider . "0.7.0"))
|
|
|
|
|
|
;; Colors & Theme Support
|
|
|
|
(defvar cider-docview-code-background-color
|
|
(cider-scale-background-color)
|
|
"Background color for code blocks.")
|
|
|
|
(defadvice enable-theme (after cider-docview-adapt-to-theme activate)
|
|
"When theme is changed, update `cider-docview-code-background-color'."
|
|
(setq cider-docview-code-background-color (cider-scale-background-color)))
|
|
|
|
|
|
(defadvice disable-theme (after cider-docview-adapt-to-theme activate)
|
|
"When theme is disabled, update `cider-docview-code-background-color'."
|
|
(setq cider-docview-code-background-color (cider-scale-background-color)))
|
|
|
|
|
|
;; Mode & key bindings
|
|
|
|
(defvar cider-docview-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map "q" #'cider-popup-buffer-quit-function)
|
|
(define-key map "g" #'cider-docview-grimoire)
|
|
(define-key map "G" #'cider-docview-grimoire-web)
|
|
(define-key map "j" #'cider-docview-javadoc)
|
|
(define-key map "s" #'cider-docview-source)
|
|
(define-key map (kbd "<backtab>") #'backward-button)
|
|
(define-key map (kbd "TAB") #'forward-button)
|
|
(easy-menu-define cider-docview-mode-menu map
|
|
"Menu for CIDER's doc mode"
|
|
`("CiderDoc"
|
|
["Look up in Grimoire" cider-docview-grimoire]
|
|
["Look up in Grimoire (browser)" cider-docview-grimoire-web]
|
|
["JavaDoc in browser" cider-docview-javadoc]
|
|
["Jump to source" cider-docview-source]
|
|
"--"
|
|
["Quit" cider-popup-buffer-quit-function]
|
|
))
|
|
map))
|
|
|
|
(defvar cider-docview-symbol)
|
|
(defvar cider-docview-javadoc-url)
|
|
(defvar cider-docview-file)
|
|
(defvar cider-docview-line)
|
|
|
|
(define-derived-mode cider-docview-mode help-mode "Doc"
|
|
"Major mode for displaying CIDER documentation
|
|
|
|
\\{cider-docview-mode-map}"
|
|
(setq buffer-read-only t)
|
|
(setq-local sesman-system 'CIDER)
|
|
(when cider-special-mode-truncate-lines
|
|
(setq-local truncate-lines t))
|
|
(setq-local electric-indent-chars nil)
|
|
(setq-local cider-docview-symbol nil)
|
|
(setq-local cider-docview-javadoc-url nil)
|
|
(setq-local cider-docview-file nil)
|
|
(setq-local cider-docview-line nil))
|
|
|
|
|
|
;;; Interactive functions
|
|
|
|
(defun cider-docview-javadoc ()
|
|
"Open the Javadoc for the current class, if available."
|
|
(interactive)
|
|
(if cider-docview-javadoc-url
|
|
(browse-url cider-docview-javadoc-url)
|
|
(error "No Javadoc available for %s" cider-docview-symbol)))
|
|
|
|
(defun cider-javadoc-handler (symbol-name)
|
|
"Invoke the nREPL \"info\" op on SYMBOL-NAME if available."
|
|
(when symbol-name
|
|
(let* ((info (cider-var-info symbol-name))
|
|
(url (nrepl-dict-get info "javadoc")))
|
|
(if url
|
|
(browse-url url)
|
|
(user-error "No Javadoc available for %s" symbol-name)))))
|
|
|
|
(defun cider-javadoc (arg)
|
|
"Open Javadoc documentation in a popup buffer.
|
|
|
|
Prompts for the symbol to use, or uses the symbol at point, depending on
|
|
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
|
|
opposite of what that option dictates."
|
|
(interactive "P")
|
|
(cider-ensure-connected)
|
|
(cider-ensure-op-supported "info")
|
|
(funcall (cider-prompt-for-symbol-function arg)
|
|
"Javadoc for"
|
|
#'cider-javadoc-handler))
|
|
|
|
(defun cider-docview-source ()
|
|
"Open the source for the current symbol, if available."
|
|
(interactive)
|
|
(if cider-docview-file
|
|
(if-let* ((buffer (and (not (cider--tooling-file-p cider-docview-file))
|
|
(cider-find-file cider-docview-file))))
|
|
(cider-jump-to buffer (if cider-docview-line
|
|
(cons cider-docview-line nil)
|
|
cider-docview-symbol)
|
|
nil)
|
|
(user-error
|
|
(substitute-command-keys
|
|
"Can't find the source because it wasn't defined with `cider-eval-buffer'")))
|
|
(error "No source location for %s" cider-docview-symbol)))
|
|
|
|
(defvar cider-buffer-ns)
|
|
|
|
(declare-function cider-grimoire-lookup "cider-grimoire")
|
|
|
|
(defun cider-docview-grimoire ()
|
|
"Return the grimoire documentation for `cider-docview-symbol'."
|
|
(interactive)
|
|
(if cider-buffer-ns
|
|
(cider-grimoire-lookup cider-docview-symbol)
|
|
(error "%s cannot be looked up on Grimoire" cider-docview-symbol)))
|
|
|
|
(declare-function cider-grimoire-web-lookup "cider-grimoire")
|
|
|
|
(defun cider-docview-grimoire-web ()
|
|
"Open the grimoire documentation for `cider-docview-symbol' in a web browser."
|
|
(interactive)
|
|
(if cider-buffer-ns
|
|
(cider-grimoire-web-lookup cider-docview-symbol)
|
|
(error "%s cannot be looked up on Grimoire" cider-docview-symbol)))
|
|
|
|
(defconst cider-doc-buffer "*cider-doc*")
|
|
|
|
(defun cider-create-doc-buffer (symbol)
|
|
"Populates *cider-doc* with the documentation for SYMBOL."
|
|
(when-let* ((info (cider-var-info symbol)))
|
|
(cider-docview-render (cider-make-popup-buffer cider-doc-buffer nil 'ancillary) symbol info)))
|
|
|
|
(defun cider-doc-lookup (symbol)
|
|
"Look up documentation for SYMBOL."
|
|
(if-let* ((buffer (cider-create-doc-buffer symbol)))
|
|
(cider-popup-buffer-display buffer cider-doc-auto-select-buffer)
|
|
(user-error "Symbol %s not resolved" symbol)))
|
|
|
|
(defun cider-doc (&optional arg)
|
|
"Open Clojure documentation in a popup buffer.
|
|
|
|
Prompts for the symbol to use, or uses the symbol at point, depending on
|
|
the value of `cider-prompt-for-symbol'. With prefix arg ARG, does the
|
|
opposite of what that option dictates."
|
|
(interactive "P")
|
|
(cider-ensure-connected)
|
|
(funcall (cider-prompt-for-symbol-function arg)
|
|
"Doc for"
|
|
#'cider-doc-lookup))
|
|
|
|
|
|
;;; Font Lock and Formatting
|
|
|
|
(defun cider-docview-fontify-code-blocks (buffer mode)
|
|
"Font lock BUFFER code blocks using MODE and remove markdown characters.
|
|
This processes the triple backtick GFM markdown extension. An overlay is used
|
|
to shade the background. Blocks are marked to be ignored by other fonification
|
|
and line wrap."
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(while (search-forward-regexp "```\n" nil t)
|
|
(replace-match "")
|
|
(let ((beg (point))
|
|
(bg `(:background ,cider-docview-code-background-color)))
|
|
(when (search-forward-regexp "```\n" nil t)
|
|
(replace-match "")
|
|
(cider-font-lock-region-as mode beg (point))
|
|
(overlay-put (make-overlay beg (point)) 'font-lock-face bg)
|
|
(put-text-property beg (point) 'block 'code)))))))
|
|
|
|
(defun cider-docview-fontify-literals (buffer)
|
|
"Font lock BUFFER literal text and remove backtick markdown characters.
|
|
Preformatted code text blocks are ignored."
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(while (search-forward "`" nil t)
|
|
(if (eq (get-text-property (point) 'block) 'code)
|
|
(forward-char)
|
|
(progn
|
|
(replace-match "")
|
|
(let ((beg (point)))
|
|
(when (search-forward "`" (line-end-position) t)
|
|
(replace-match "")
|
|
(put-text-property beg (point) 'font-lock-face 'cider-docview-literal-face)))))))))
|
|
|
|
(defun cider-docview-fontify-emphasis (buffer)
|
|
"Font lock BUFFER emphasized text and remove markdown characters.
|
|
One '*' represents emphasis, multiple '**'s represent strong emphasis.
|
|
Preformatted code text blocks are ignored."
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(while (search-forward-regexp "\\(*+\\)\\(\\w\\)" nil t)
|
|
(if (eq (get-text-property (point) 'block) 'code)
|
|
(forward-char)
|
|
(progn
|
|
(replace-match "\\2")
|
|
(let ((beg (1- (point)))
|
|
(face (if (> (length (match-string 1)) 1)
|
|
'cider-docview-strong-face
|
|
'cider-docview-emphasis-face)))
|
|
(when (search-forward-regexp "\\(\\w\\)\\*+" (line-end-position) t)
|
|
(replace-match "\\1")
|
|
(put-text-property beg (point) 'font-lock-face face)))))))))
|
|
|
|
(defun cider-docview-format-tables (buffer)
|
|
"Align BUFFER tables and dim borders.
|
|
This processes the GFM table markdown extension using `org-table'.
|
|
Tables are marked to be ignored by line wrap."
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(let ((border 'cider-docview-table-border-face))
|
|
(org-table-map-tables
|
|
(lambda ()
|
|
(org-table-align)
|
|
(goto-char (org-table-begin))
|
|
(while (search-forward-regexp "[+|-]" (org-table-end) t)
|
|
(put-text-property (match-beginning 0) (match-end 0) 'font-lock-face border))
|
|
(put-text-property (org-table-begin) (org-table-end) 'block 'table)))))))
|
|
|
|
(defun cider-docview-wrap-text (buffer)
|
|
"For text in BUFFER not propertized as 'block', apply line wrap."
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(while (not (eobp))
|
|
(unless (get-text-property (point) 'block)
|
|
(fill-region (point) (line-end-position)))
|
|
(forward-line)))))
|
|
|
|
|
|
;;; Rendering
|
|
|
|
(defun cider-docview-render-java-doc (buffer text)
|
|
"Emit into BUFFER formatted doc TEXT for a Java class or member."
|
|
(with-current-buffer buffer
|
|
(let ((beg (point)))
|
|
(insert text)
|
|
(save-excursion
|
|
(goto-char beg)
|
|
(cider-docview-fontify-code-blocks buffer 'java-mode) ; left alone hereafter
|
|
(cider-docview-fontify-literals buffer)
|
|
(cider-docview-fontify-emphasis buffer)
|
|
(cider-docview-format-tables buffer) ; may contain literals, emphasis
|
|
(cider-docview-wrap-text buffer))))) ; ignores code, table blocks
|
|
|
|
(defun cider--abbreviate-file-protocol (file-with-protocol)
|
|
"Abbreviate the file-path in `file:/path/to/file' of FILE-WITH-PROTOCOL."
|
|
(if (string-match "\\`file:\\(.*\\)" file-with-protocol)
|
|
(let ((file (match-string 1 file-with-protocol))
|
|
(proj-dir (clojure-project-dir)))
|
|
(if (and proj-dir
|
|
(file-in-directory-p file proj-dir))
|
|
(file-relative-name file proj-dir)
|
|
file))
|
|
file-with-protocol))
|
|
|
|
(defun cider-docview-render-info (buffer info)
|
|
"Emit into BUFFER formatted INFO for the Clojure or Java symbol."
|
|
(let* ((ns (nrepl-dict-get info "ns"))
|
|
(name (nrepl-dict-get info "name"))
|
|
(added (nrepl-dict-get info "added"))
|
|
(depr (nrepl-dict-get info "deprecated"))
|
|
(macro (nrepl-dict-get info "macro"))
|
|
(special (nrepl-dict-get info "special-form"))
|
|
(forms (when-let* ((str (nrepl-dict-get info "forms-str")))
|
|
(split-string str "\n")))
|
|
(args (when-let* ((str (nrepl-dict-get info "arglists-str")))
|
|
(split-string str "\n")))
|
|
(doc (or (nrepl-dict-get info "doc")
|
|
"Not documented."))
|
|
(url (nrepl-dict-get info "url"))
|
|
(class (nrepl-dict-get info "class"))
|
|
(member (nrepl-dict-get info "member"))
|
|
(javadoc (nrepl-dict-get info "javadoc"))
|
|
(super (nrepl-dict-get info "super"))
|
|
(ifaces (nrepl-dict-get info "interfaces"))
|
|
(spec (nrepl-dict-get info "spec"))
|
|
(clj-name (if ns (concat ns "/" name) name))
|
|
(java-name (if member (concat class "/" member) class))
|
|
(see-also (nrepl-dict-get info "see-also")))
|
|
(cider--help-setup-xref (list #'cider-doc-lookup (format "%s/%s" ns name)) nil buffer)
|
|
(with-current-buffer buffer
|
|
(cl-flet ((emit (text &optional face)
|
|
(insert (if face
|
|
(propertize text 'font-lock-face face)
|
|
text)
|
|
"\n")))
|
|
(emit (if class java-name clj-name) 'font-lock-function-name-face)
|
|
(when super
|
|
(emit (concat " Extends: " (cider-font-lock-as 'java-mode super))))
|
|
(when ifaces
|
|
(emit (concat "Implements: " (cider-font-lock-as 'java-mode (car ifaces))))
|
|
(dolist (iface (cdr ifaces))
|
|
(emit (concat " "(cider-font-lock-as 'java-mode iface)))))
|
|
(when (or super ifaces)
|
|
(insert "\n"))
|
|
(when-let* ((forms (or forms args)))
|
|
(dolist (form forms)
|
|
(insert " ")
|
|
(emit (cider-font-lock-as-clojure form))))
|
|
(when special
|
|
(emit "Special Form" 'font-lock-keyword-face))
|
|
(when macro
|
|
(emit "Macro" 'font-lock-variable-name-face))
|
|
(when added
|
|
(emit (concat "Added in " added) 'font-lock-comment-face))
|
|
(when depr
|
|
(emit (concat "Deprecated in " depr) 'font-lock-keyword-face))
|
|
(if class
|
|
(cider-docview-render-java-doc (current-buffer) doc)
|
|
(emit (concat " " doc)))
|
|
(when url
|
|
(insert "\n Please see ")
|
|
(insert-text-button url
|
|
'url url
|
|
'follow-link t
|
|
'action (lambda (x)
|
|
(browse-url (button-get x 'url))))
|
|
(insert "\n"))
|
|
(when javadoc
|
|
(insert "\n\nFor additional documentation, see the ")
|
|
(insert-text-button "Javadoc"
|
|
'url javadoc
|
|
'follow-link t
|
|
'action (lambda (x)
|
|
(browse-url (button-get x 'url))))
|
|
(insert ".\n"))
|
|
(insert "\n")
|
|
(when spec
|
|
(emit "Spec:" 'font-lock-function-name-face)
|
|
(insert (cider-browse-spec--pprint-indented spec))
|
|
(insert "\n\n")
|
|
(insert-text-button "Browse spec"
|
|
'follow-link t
|
|
'action (lambda (_)
|
|
(cider-browse-spec (format "%s/%s" ns name))))
|
|
(insert "\n\n"))
|
|
(if cider-docview-file
|
|
(progn
|
|
(insert (propertize (if class java-name clj-name)
|
|
'font-lock-face 'font-lock-function-name-face)
|
|
" is defined in ")
|
|
(insert-text-button (cider--abbreviate-file-protocol cider-docview-file)
|
|
'follow-link t
|
|
'action (lambda (_x)
|
|
(cider-docview-source)))
|
|
(insert "."))
|
|
(insert "Definition location unavailable."))
|
|
(when see-also
|
|
(insert "\n\n Also see: ")
|
|
(mapc (lambda (ns-sym)
|
|
(let* ((ns-sym-split (split-string ns-sym "/"))
|
|
(see-also-ns (car ns-sym-split))
|
|
(see-also-sym (cadr ns-sym-split))
|
|
;; if the var belongs to the same namespace,
|
|
;; we omit the namespace to save some screen space
|
|
(symbol (if (equal ns see-also-ns) see-also-sym ns-sym)))
|
|
(insert-text-button symbol
|
|
'type 'help-xref
|
|
'help-function (apply-partially #'cider-doc-lookup symbol)))
|
|
(insert " "))
|
|
see-also))
|
|
(cider--doc-make-xrefs)
|
|
(let ((beg (point-min))
|
|
(end (point-max)))
|
|
(nrepl-dict-map (lambda (k v)
|
|
(put-text-property beg end k v))
|
|
info)))
|
|
(current-buffer))))
|
|
|
|
(declare-function cider-set-buffer-ns "cider-mode")
|
|
(defun cider-docview-render (buffer symbol info)
|
|
"Emit into BUFFER formatted documentation for SYMBOL's INFO."
|
|
(with-current-buffer buffer
|
|
(let ((javadoc (nrepl-dict-get info "javadoc"))
|
|
(file (nrepl-dict-get info "file"))
|
|
(line (nrepl-dict-get info "line"))
|
|
(ns (nrepl-dict-get info "ns"))
|
|
(inhibit-read-only t))
|
|
(cider-docview-mode)
|
|
|
|
(cider-set-buffer-ns ns)
|
|
(setq-local cider-docview-symbol symbol)
|
|
(setq-local cider-docview-javadoc-url javadoc)
|
|
(setq-local cider-docview-file file)
|
|
(setq-local cider-docview-line line)
|
|
|
|
(remove-overlays)
|
|
(cider-docview-render-info buffer info)
|
|
|
|
(goto-char (point-min))
|
|
(current-buffer))))
|
|
|
|
|
|
(provide 'cider-doc)
|
|
|
|
;;; cider-doc.el ends here
|