;;; geiser-doc.el -- accessing scheme-provided documentation ;; Copyright (C) 2009-2016, 2021-2022 Jose Antonio Ortega Ruiz ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the Modified BSD License. You should ;; have received a copy of the license along with this program. If ;; not, see . ;; Start date: Sat Feb 14, 2009 14:09 ;;; Code: (require 'geiser-edit) (require 'geiser-impl) (require 'geiser-completion) (require 'geiser-autodoc) (require 'geiser-eval) (require 'geiser-syntax) (require 'geiser-menu) (require 'geiser-popup) (require 'geiser-custom) (require 'geiser-base) (require 'button) ;;; Customization: (defgroup geiser-doc nil "Options for documentation buffers." :group 'geiser) (geiser-custom--defface doc-title 'bold geiser-doc "article titles in documentation buffers") (geiser-custom--defface doc-link 'link geiser-doc "links in documentation buffers") (geiser-custom--defface doc-button 'button geiser-doc "buttons in documentation buffers") ;;; Implementation (geiser-impl--define-caller geiser-doc--external-help external-help (symbol module) "By default, Geiser will display help about an identifier in a help buffer, after collecting the associated signature and docstring. You can provide an alternative function for displaying help (e.g. browse an HTML page) implementing this method.") ;;; Documentation browser history: (defvar geiser-doc-history-size 50) (defvar geiser-doc--history nil) (defun geiser-doc--make-history () (list nil ; current (make-ring geiser-doc-history-size) ; previous (make-ring geiser-doc-history-size))) ; next (setq geiser-doc--history (geiser-doc--make-history)) (defsubst geiser-doc--history-current () (car geiser-doc--history)) (defsubst geiser-doc--history-previous-link () (ring-ref (cadr geiser-doc--history) 0)) (defsubst geiser-doc--history-next-link () (ring-ref (car (cddr geiser-doc--history)) 0)) (defun geiser-doc--history-push (link) (unless (or (null link) (equal link (geiser-doc--history-current))) (when (not (null (geiser-doc--history-current))) (let ((next (geiser-doc--history-next))) (unless (equal link next) (when next (geiser-doc--history-previous)) (ring-insert (nth 1 geiser-doc--history) (car geiser-doc--history))))) (setcar geiser-doc--history link)) link) (defsubst geiser-doc--history-next-p () (not (ring-empty-p (nth 2 geiser-doc--history)))) (defun geiser-doc--history-next (&optional forget-current) (when (geiser-doc--history-next-p) (when (and (car geiser-doc--history) (not forget-current)) (ring-insert (nth 1 geiser-doc--history) (car geiser-doc--history))) (setcar geiser-doc--history (ring-remove (nth 2 geiser-doc--history) 0)))) (defsubst geiser-doc--history-previous-p () (not (ring-empty-p (nth 1 geiser-doc--history)))) (defun geiser-doc--history-previous (&optional forget-current) (when (geiser-doc--history-previous-p) (when (and (car geiser-doc--history) (not forget-current)) (ring-insert (nth 2 geiser-doc--history) (car geiser-doc--history))) (setcar geiser-doc--history (ring-remove (nth 1 geiser-doc--history) 0)))) ;;; Links (defsubst geiser-doc--make-link (target module impl) (list target module impl)) (defsubst geiser-doc--link-target (link) (nth 0 link)) (defsubst geiser-doc--link-module (link) (nth 1 link)) (defsubst geiser-doc--link-impl (link) (nth 2 link)) (defun geiser-doc--follow-link (link) (let ((target (geiser-doc--link-target link)) (module (geiser-doc--link-module link)) (impl (geiser-doc--link-impl link))) (when (and (or target module) impl) (with--geiser-implementation impl (if (null target) (geiser-doc-module module impl) (let ((geiser-eval--get-module-function (lambda (x) module))) (geiser-doc-symbol target module impl))))))) (defvar-local geiser-doc--buffer-link nil) (defsubst geiser-doc--implementation () (geiser-doc--link-impl geiser-doc--buffer-link)) (defun geiser-doc--button-action (button) (let ((link (button-get button 'geiser-link))) (when link (geiser-doc--follow-link link)))) (define-button-type 'geiser-doc--button 'action 'geiser-doc--button-action 'follow-link t) (defun geiser-doc--make-module-button (beg end module impl) (let ((link (geiser-doc--make-link nil module impl)) (help (format "Help for module %s" module))) (make-text-button beg end :type 'geiser-doc--button 'face 'geiser-font-lock-doc-link 'geiser-link link 'help-echo help))) (defun geiser-doc--insert-button (target module impl &optional sign) (let* ((link (geiser-doc--make-link target module impl)) (sign (when sign (if (listp sign) sign (list target)))) (text (format "%s" (or (and sign (geiser-autodoc--str* sign)) target module))) (help (format "%smodule %s" (if target (format "%s in " target) "") (or module "")))) (insert-text-button text :type 'geiser-doc--button 'face 'geiser-font-lock-doc-link 'geiser-link link 'help-echo help))) (defun geiser-doc-goto-source () "Go to the definition of this item." (interactive) (when-let (link geiser-doc--buffer-link) (with--geiser-implementation (geiser-doc--link-impl link) (if-let (target (geiser-doc--link-target link)) (geiser-edit-symbol target nil (point-marker)) (geiser-edit-module (geiser-doc--link-module link)))))) (defun geiser-doc-goto-manual () "Go to the manual for this item." (interactive) (when-let (link geiser-doc--buffer-link) (let ((tm (geiser-doc--link-target link)) (mod (geiser-doc--link-module link)) (impl (geiser-doc--link-impl link))) (geiser-doc--external-help impl (or tm mod) mod)))) (defun geiser-doc--xbutton-action (button) (let ((k (button-get button 'x-kind))) (cond ((eq 'source k) (geiser-doc-goto-source)) ((eq 'manual k) (geiser-doc-goto-manual))))) (define-button-type 'geiser-doc--xbutton 'action 'geiser-doc--xbutton-action 'face 'geiser-font-lock-doc-button 'follow-link t) (defun geiser-doc--insert-xbutton (&optional manual) (let ((label (if manual "[manual]" "[source]")) (help (if manual "Look up in Scheme manual" "Go to definition"))) (insert-text-button label :type 'geiser-doc--xbutton 'help-echo help 'x-kind (if manual 'manual 'source)))) (defun geiser-doc--insert-xbuttons (impl) (when (geiser-impl--method 'external-help impl) (geiser-doc--insert-xbutton t) (insert " ")) (geiser-doc--insert-xbutton)) (defun geiser-doc--insert-nav-button (next) (let* ((lnk (if next (geiser-doc--history-next-link) (geiser-doc--history-previous-link))) (what (geiser-doc--link-target lnk)) (what (or what (geiser-doc--link-module lnk))) (action (if next '(lambda (b) (geiser-doc-next)) '(lambda (b) (geiser-doc-previous))))) (insert-text-button (if next "[forward]" "[back]") 'action action 'help-echo (format "Previous help item (%s)" what) 'face 'geiser-font-lock-doc-button 'follow-link t))) ;;; Auxiliary functions: (defun geiser-doc--manual-available-p () (geiser-impl--method 'external-help geiser-impl--implementation)) (defun geiser-doc--module (&optional mod impl) (let ((impl (or impl (geiser-doc--link-impl geiser-doc--buffer-link))) (mod (or mod (geiser-doc--link-module geiser-doc--buffer-link)))) (geiser-impl--call-method 'find-module impl mod))) (defun geiser-doc--insert-title (title) (let ((p (point))) (insert (format "%s" title)) (fill-paragraph nil) (let ((indent-line-function 'lisp-indent-line)) (indent-region p (point))) (put-text-property p (point) 'face 'geiser-font-lock-doc-title) (newline))) (defun geiser-doc--insert-list (title lst module impl) (when lst (geiser-doc--insert-title title) (newline) (dolist (w lst) (let ((name (car w)) (signature (cdr (assoc "signature" w))) (info (cdr (assoc "info" w)))) (insert "\t- ") (if module (geiser-doc--insert-button name module impl signature) (geiser-doc--insert-button nil name impl)) (when info (insert (format " %s" info))) (newline))) (newline))) (defun geiser-doc--insert-footer (impl) (newline 2) (geiser-doc--insert-xbuttons impl) (let* ((prev (and (geiser-doc--history-previous-p) 8)) (nxt (and (geiser-doc--history-next-p) 10)) (len (max 1 (- (window-width) (- (point) (line-beginning-position)) (or prev 0) (or nxt 0))))) (when (or prev nxt) (insert (make-string len ?\ ))) (when prev (geiser-doc--insert-nav-button nil) (insert " ")) (when nxt (geiser-doc--insert-nav-button t)))) ;;; Documentation browser and mode: (defun geiser-doc-edit-symbol-at-point () "Open definition of symbol at point." (interactive) (let* ((impl (geiser-doc--implementation)) (module (geiser-doc--module))) (unless (and impl module) (error "I don't know what module this buffer refers to.")) (with--geiser-implementation impl (geiser-edit-symbol-at-point)))) (defvar geiser-doc-mode-map (let ((map (make-sparse-keymap))) (suppress-keymap map) (set-keymap-parent map button-buffer-map) map) "Keymap for `geiser-doc-mode'.") (declare-function switch-to-geiser "geiser-repl") (defun geiser-doc-switch-to-repl () (interactive) (switch-to-geiser nil nil (current-buffer))) (geiser-menu--defmenu doc geiser-doc-mode-map ("Next link" ("n") forward-button) ("Previous link" ("p") backward-button) ("Next section" ("N") geiser-doc-next-section) ("Previous section" ("P") geiser-doc-previous-section) -- ("Next page" ("f") geiser-doc-next "Next item" :enable (geiser-doc--history-next-p)) ("Previous page" ("b") geiser-doc-previous "Previous item" :enable (geiser-doc--history-previous-p)) -- ("Go to REPL" ("z" "\C-cz" "\C-c\C-z") geiser-doc-switch-to-repl) ("Refresh" ("g" "r") geiser-doc-refresh "Refresh current page") -- ("Edit symbol" ("." "\M-.") geiser-doc-edit-symbol-at-point :enable (geiser--symbol-at-point)) ("View source" ("s") geiser-doc-goto-source) ("View manual" ("m" "h") geiser-doc-goto-manual) -- ("Kill item" "k" geiser-doc-kill-page "Kill this page") ("Clear history" "c" geiser-doc-clean-history) -- (custom "Browser options" geiser-doc) -- ("Quit" nil View-quit)) (define-derived-mode geiser-doc-mode nil "Geiser Doc" "Major mode for browsing scheme documentation. \\{geiser-doc-mode-map}" (buffer-disable-undo) (setq truncate-lines t) (set-syntax-table scheme-mode-syntax-table) (setq geiser-eval--get-module-function 'geiser-doc--module) (setq buffer-read-only t)) (geiser-popup--define doc "*Geiser Documentation*" geiser-doc-mode) ;;; Commands: (defun geiser-doc--get-docstring (symbol module) (geiser-eval--send/result `(:eval (:ge symbol-documentation ',symbol) ,module))) (defun geiser-doc--get-module-exports (module) (geiser-eval--send/result `(:eval (:ge module-exports '(:module ,module)) :f))) (defun geiser-doc--buttonize-modules (impl) (save-excursion (goto-char (point-min)) (while (re-search-forward "in module \\([^.\n]+\\)[.\n ]" nil t) (geiser-doc--make-module-button (match-beginning 1) (match-end 1) (geiser-doc--module (match-string 1) impl) impl)))) (defun geiser-doc--render-docstring (docstring symbol &optional module impl) (erase-buffer) (geiser-doc--insert-title (geiser-autodoc--str* (cdr (assoc "signature" docstring)))) (newline) (insert (or (cdr (assoc "docstring" docstring)) "")) (geiser-doc--buttonize-modules impl) (setq geiser-doc--buffer-link (geiser-doc--history-push (geiser-doc--make-link symbol module impl))) (geiser-doc--insert-footer impl) (goto-char (point-min))) (defun geiser-doc-symbol (symbol &optional module impl) (let* ((impl (or impl geiser-impl--implementation)) (module (geiser-doc--module (or module (geiser-eval--get-module)) impl))) (let ((ds (geiser-doc--get-docstring symbol module))) (if (or (not ds) (not (listp ds))) (message "No documentation available for '%s'" symbol) (geiser-doc--with-buffer (geiser-doc--render-docstring ds symbol module impl)) (geiser-doc--pop-to-buffer))))) (defun geiser-doc-symbol-at-point (&optional arg) "Get docstring for symbol at point. With prefix argument, ask for symbol (with completion)." (interactive "P") (let ((symbol (or (and (not arg) (geiser--symbol-at-point)) (geiser-completion--read-symbol "Symbol: " (geiser--symbol-at-point))))) (when symbol (geiser-doc-symbol symbol)))) (defun geiser-doc-manual-for-symbol (symbol) (geiser-doc--external-help geiser-impl--implementation symbol (geiser-eval--get-module))) (defun geiser-doc-look-up-manual (&optional arg) "Look up manual for symbol at point. With prefix argument, ask for the lookup symbol (with completion)." (interactive "P") (unless (geiser-doc--manual-available-p) (error "No manual available")) (let ((symbol (or (and (not arg) (geiser--symbol-at-point)) (geiser-completion--read-symbol "Symbol: ")))) (geiser-doc-manual-for-symbol symbol))) (defconst geiser-doc--sections '(("Procedures:" "procs") ("Syntax:" "syntax") ("Variables:" "vars") ("Submodules:" "modules" t))) (defconst geiser-doc--sections-re (format "^%s\n" (regexp-opt (mapcar 'car geiser-doc--sections)))) (defun geiser-doc-module (&optional module impl) "Display information about a given module." (interactive) (let* ((impl (or impl geiser-impl--implementation)) (module (geiser-doc--module (or module (geiser-completion--read-module)) impl)) (msg (format "Retrieving documentation for %s ..." module)) (exports (progn (message "%s" msg) (geiser-doc--get-module-exports module)))) (if (not exports) (message "No information available for %s" module) (geiser-doc--with-buffer (erase-buffer) (geiser-doc--insert-title (format "%s" module)) (newline) (dolist (g geiser-doc--sections) (geiser-doc--insert-list (car g) (cdr (assoc (cadr g) exports)) (and (not (cddr g)) module) impl)) (setq geiser-doc--buffer-link (geiser-doc--history-push (geiser-doc--make-link nil module impl))) (geiser-doc--insert-footer impl) (goto-char (point-min))) (message "%s done" msg) (geiser-doc--pop-to-buffer)))) (defun geiser-doc-next-section () "Move to next section in this page." (interactive) (forward-line) (re-search-forward geiser-doc--sections-re nil t) (forward-line -1)) (defun geiser-doc-previous-section () "Move to previous section in this page." (interactive) (re-search-backward geiser-doc--sections-re nil t)) (defun geiser-doc-next (&optional forget-current) "Go to next page in documentation browser. With prefix, the current page is deleted from history." (interactive "P") (let ((link (geiser-doc--history-next forget-current))) (unless link (error "No next page")) (geiser-doc--follow-link link))) (defun geiser-doc-previous (&optional forget-current) "Go to previous page in documentation browser. With prefix, the current page is deleted from history." (interactive "P") (let ((link (geiser-doc--history-previous forget-current))) (unless link (error "No previous page")) (geiser-doc--follow-link link))) (defun geiser-doc-kill-page () "Kill current page if a previous or next one exists." (interactive) (condition-case nil (geiser-doc-previous t) (error (geiser-doc-next t)))) (defun geiser-doc-refresh () "Refresh the contents of current page." (interactive) (when geiser-doc--buffer-link (geiser-doc--follow-link geiser-doc--buffer-link))) (defun geiser-doc-clean-history () "Clean up the document browser history." (interactive) (when (y-or-n-p "Clean browsing history? ") (setq geiser-doc--history (geiser-doc--make-history)) (geiser-doc-refresh)) (message "")) (provide 'geiser-doc)