1039 lines
39 KiB
EmacsLisp
1039 lines
39 KiB
EmacsLisp
;;; ess-roxy.el --- convenient editing of in-code roxygen documentation -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2009-2022 Free Software Foundation, Inc.
|
||
;; Author: Henning Redestig <henning.red * go0glemail c-m>
|
||
|
||
;; This file is part of GNU Emacs.
|
||
|
||
;;; License:
|
||
;;
|
||
;; 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:
|
||
|
||
;; Lots of inspiration from doc-mode,
|
||
;; https://nschum.de/src/emacs/doc-mode/
|
||
;;
|
||
;; Features::
|
||
;;
|
||
;; - basic highlighting
|
||
;; - generating and updating templates from function definition and customized default template
|
||
;; - C-c C-o C-o :: update template
|
||
;; - navigating and filling roxygen fields
|
||
;; - C-c TAB, M-q, C-a, ENTER, M-h :: advised tag completion, fill-paragraph,
|
||
;; ess-roxy-move-beginning-of-line, newline-and-indent
|
||
;; - C-c C-o n,p :: next, previous roxygen entry
|
||
;; - C-c C-o C-c :: Unroxygen region. Convenient for editing examples.
|
||
;; - folding visibility using hs-minor-mode
|
||
;; - TAB :: advised ess-indent-command, hide entry if in roxygen doc.
|
||
;; - preview
|
||
;; - C-c C-o C-r :: create a preview of the Rd file as generated
|
||
;; using roxygen
|
||
;; - C-c C-o C-t :: create a preview of the Rd HTML file as generated
|
||
;; using roxygen and the tools package
|
||
;; - C-c C-o t :: create a preview of the Rd text file
|
||
;;
|
||
;; Known issues:
|
||
;;
|
||
;; - hideshow mode does not work very well. In particular, if ordinary
|
||
;; comments precede a roxygen entry, then both will be hidden in the
|
||
;; same overlay from start and not unfoldable using TAB since the
|
||
;; roxygen prefix is not present. The planned solution is implement
|
||
;; a replacement for hideshow.
|
||
;; - only limited functionality for S4 documentation.
|
||
|
||
;;; Code:
|
||
|
||
(require 'ess-utils)
|
||
(require 'hideshow)
|
||
(require 'outline)
|
||
(eval-when-compile
|
||
(require 'cl-lib)
|
||
(require 'subr-x))
|
||
(require 'ess-rd)
|
||
(require 'ess-r-syntax)
|
||
|
||
(defvar roxy-str)
|
||
(defvar ess-r-mode-syntax-table)
|
||
(declare-function ess-fill-args "ess-r-mode")
|
||
(declare-function ess-fill-continuations "ess-r-mode")
|
||
(declare-function inferior-ess-r-force "ess-r-mode")
|
||
|
||
(defvar-local ess-roxy-re nil
|
||
"Regular expression to recognize roxygen blocks.")
|
||
|
||
|
||
;;*;; Roxy Minor Mode
|
||
|
||
(defvar ess-roxy-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "C-c C-o h") #'ess-roxy-hide-all)
|
||
(define-key map (kbd "C-c C-o n") #'ess-roxy-next-entry)
|
||
(define-key map (kbd "C-c C-o p") #'ess-roxy-previous-entry)
|
||
(define-key map (kbd "C-c C-o C-o") #'ess-roxy-update-entry)
|
||
(define-key map (kbd "C-c C-o C-r") #'ess-roxy-preview-Rd)
|
||
(define-key map (kbd "C-c C-o C-w") #'ess-roxy-preview-HTML)
|
||
(define-key map (kbd "C-c C-o C-t") #'ess-roxy-preview-text)
|
||
(define-key map (kbd "C-c C-o C-c") #'ess-roxy-toggle-roxy-region)
|
||
(define-key map [remap back-to-indentation] #'ess-roxy-goto-end-of-roxy-comment)
|
||
(define-key map [remap newline] #'ess-roxy-newline)
|
||
(define-key map [remap newline-and-indent] #'ess-roxy-newline)
|
||
(define-key map [remap ess-indent-command] #'ess-roxy-ess-indent-command)
|
||
(define-key map [remap move-beginning-of-line] #'ess-roxy-move-beginning-of-line)
|
||
(define-key map [remap beginning-of-visual-line] #'ess-roxy-move-beginning-of-line)
|
||
map))
|
||
|
||
(defvar ess-roxy-font-lock-keywords nil
|
||
"Cache set by `ess-roxy-generate-keywords'.
|
||
Used to remove keywords added by function `ess-roxy-mode'.")
|
||
|
||
(defun ess-roxy-generate-keywords ()
|
||
"Generate a list of keywords suitable for `font-lock-add-keywords'."
|
||
(setq-local ess-roxy-font-lock-keywords
|
||
`((,(concat ess-roxy-re " *\\([@\\]"
|
||
(regexp-opt ess-roxy-tags-param t)
|
||
"\\)\\>")
|
||
(1 'font-lock-keyword-face prepend))
|
||
(,(concat ess-roxy-re " *\\(@"
|
||
(regexp-opt '("param" "importFrom" "importClassesFrom"
|
||
"importMethodsFrom" "describeIn")
|
||
'words)
|
||
"\\)\\(?:[ \t]+\\(\\(?:\\sw+,?\\)+\\)\\)")
|
||
(1 'font-lock-keyword-face prepend)
|
||
(3 'font-lock-variable-name-face prepend))
|
||
(,(concat "[@\\]" (regexp-opt ess-roxy-tags-noparam t) "\\>")
|
||
(0 'font-lock-variable-name-face prepend))
|
||
(,(concat ess-roxy-re)
|
||
(0 'bold prepend)))))
|
||
|
||
(defvar ess-roxy-fold-examples nil
|
||
"Whether to fold `@examples' when opening a buffer.
|
||
Use you regular key for `outline-show-entry' to reveal it.")
|
||
|
||
;;;###autoload
|
||
(define-minor-mode ess-roxy-mode
|
||
"Minor mode for editing ROxygen documentation."
|
||
:keymap ess-roxy-mode-map
|
||
(if ess-roxy-mode
|
||
;; Turn on `ess-roxy-mode':
|
||
(progn
|
||
(setq-local ess-roxy-re (concat "^" (string-trim comment-start) "+'"))
|
||
(font-lock-add-keywords nil (ess-roxy-generate-keywords))
|
||
(add-hook 'completion-at-point-functions #'ess-roxy-complete-tag nil t)
|
||
;; Hideshow Integration
|
||
(when ess-roxy-hide-show-p
|
||
(hs-minor-mode 1)
|
||
(when ess-roxy-start-hidden-p
|
||
(ess-roxy-hide-all)))
|
||
;; Outline Integration
|
||
(when ess-roxy-fold-examples
|
||
(ess-roxy-hide-all-examples))
|
||
;; Autofill
|
||
(setq-local paragraph-start (concat "\\(" ess-roxy-re "\\)*" paragraph-start))
|
||
(setq-local paragraph-separate (concat "\\(" ess-roxy-re "\\)*" paragraph-separate))
|
||
(setq-local adaptive-fill-function #'ess-roxy-adaptive-fill-function)
|
||
;; Hooks
|
||
(add-hook 'ess-presend-filter-functions #'ess-roxy-remove-roxy-re nil t))
|
||
;; Turn off `ess-roxy-mode':
|
||
;; Hideshow
|
||
(when (and ess-roxy-hide-show-p hs-minor-mode)
|
||
(hs-show-all)
|
||
(hs-minor-mode))
|
||
;; Hooks
|
||
(remove-hook 'ess-presend-filter-functions #'ess-roxy-remove-roxy-re t)
|
||
(font-lock-remove-keywords nil ess-roxy-font-lock-keywords)
|
||
;; (setq-local syntax-propertize-function nil)
|
||
;; (setq-local font-lock-fontify-region-function nil)
|
||
;; (setq-local font-lock-unfontify-region-function nil)
|
||
)
|
||
;; Regardless of turning on or off we need to re-fontify the buffer:
|
||
(when font-lock-mode
|
||
(font-lock-flush)))
|
||
|
||
|
||
|
||
;;*;; Outline Integration
|
||
|
||
(defvar ess-roxy-outline-regexp "^#+' +@examples\\|^[^#]")
|
||
|
||
(defun ess-roxy-substitute-outline-regexp (command)
|
||
(let ((outline-regexp (if (ess-roxy-entry-p "examples")
|
||
ess-roxy-outline-regexp
|
||
outline-regexp)))
|
||
(funcall command)))
|
||
|
||
(declare-function outline-cycle "outline-magic")
|
||
(defun ess-roxy-cycle-example ()
|
||
(interactive)
|
||
(unless (featurep 'outline-magic)
|
||
(error "Please install and load outline-magic"))
|
||
;; Don't show children when cycling @examples
|
||
(let ((this-command 'outline-cycle-overview))
|
||
(ess-roxy-substitute-outline-regexp #'outline-cycle)))
|
||
|
||
(defun ess-roxy-show-example ()
|
||
(interactive)
|
||
(ess-roxy-substitute-outline-regexp #'outline-show-entry))
|
||
|
||
(defun ess-roxy-hide-example ()
|
||
(interactive)
|
||
(ess-roxy-substitute-outline-regexp #'outline-hide-entry))
|
||
|
||
(defun ess-roxy-hide-all-examples ()
|
||
(interactive)
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(while (re-search-forward "^#+' +@examples\\b" nil t)
|
||
;; Handle edge cases
|
||
(when (ess-roxy-entry-p "examples")
|
||
(ess-roxy-hide-example)))))
|
||
|
||
(substitute-key-definition #'outline-hide-entry
|
||
#'ess-roxy-hide-example
|
||
ess-roxy-mode-map outline-minor-mode-map)
|
||
|
||
(substitute-key-definition #'outline-show-entry
|
||
#'ess-roxy-show-example
|
||
ess-roxy-mode-map outline-minor-mode-map)
|
||
|
||
|
||
;;*;; Function definitions
|
||
|
||
(defun ess-back-to-roxy ()
|
||
"Go to roxy prefix."
|
||
(end-of-line)
|
||
(re-search-backward (concat ess-roxy-re " ?") (line-beginning-position))
|
||
(goto-char (match-end 0)))
|
||
|
||
(defun ess-roxy-beg-of-entry ()
|
||
"Get point number at start of current entry, 0 if not in entry."
|
||
(save-excursion
|
||
(let (beg)
|
||
(beginning-of-line)
|
||
(setq beg -1)
|
||
(if (not (ess-roxy-entry-p))
|
||
(setq beg 0)
|
||
(setq beg (point)))
|
||
(while (and (= (forward-line -1) 0) (ess-roxy-entry-p))
|
||
(setq beg (point)))
|
||
beg)))
|
||
|
||
(defun ess-roxy-in-header-p ()
|
||
"True if point is the description / details field."
|
||
(save-excursion
|
||
(let ((res t)
|
||
(cont (ess-roxy-entry-p)))
|
||
(beginning-of-line)
|
||
(while cont
|
||
(if (looking-at (concat ess-roxy-re " *[@].+"))
|
||
(progn (setq res nil)
|
||
(setq cont nil)))
|
||
(setq cont (and (= (forward-line -1) 0) (ess-roxy-entry-p)))
|
||
)res)))
|
||
|
||
(defun ess-roxy-beg-of-field ()
|
||
"Get point number at beginning of current field, 0 if not in entry."
|
||
(save-excursion
|
||
(let (cont beg)
|
||
(beginning-of-line)
|
||
(setq beg 0)
|
||
(setq cont t)
|
||
(while (and (ess-roxy-entry-p) cont)
|
||
(setq beg (point))
|
||
(if (looking-at (concat ess-roxy-re " *[@].+"))
|
||
(setq cont nil))
|
||
(if (ess-roxy-in-header-p)
|
||
(if (looking-at (concat ess-roxy-re " *$"))
|
||
(progn
|
||
(forward-line 1)
|
||
(setq beg (point))
|
||
(setq cont nil))))
|
||
(if cont (setq cont (= (forward-line -1) 0))))
|
||
beg)))
|
||
|
||
(defun ess-roxy-end-of-entry ()
|
||
"Get point number at end of current entry, 0 if not in entry."
|
||
(save-excursion
|
||
(let ((end))
|
||
(end-of-line)
|
||
(setq end -1)
|
||
(if (not (ess-roxy-entry-p))
|
||
(setq end 0)
|
||
(setq end (point)))
|
||
(while (and (= (forward-line 1) 0) (ess-roxy-entry-p))
|
||
(end-of-line)
|
||
(setq end (point)))
|
||
end)))
|
||
|
||
(defun ess-roxy-end-of-field ()
|
||
"Get point number at end of current field, 0 if not in entry."
|
||
(save-excursion
|
||
(let ((end nil)
|
||
(cont nil))
|
||
(setq end 0)
|
||
(if (ess-roxy-entry-p) (progn (end-of-line) (setq end (point))))
|
||
(beginning-of-line)
|
||
(forward-line 1)
|
||
(setq cont t)
|
||
(while (and (ess-roxy-entry-p) cont)
|
||
(save-excursion
|
||
(end-of-line)
|
||
(setq end (point)))
|
||
(if (or (and (ess-roxy-in-header-p)
|
||
(looking-at (concat ess-roxy-re " *$")))
|
||
(looking-at (concat ess-roxy-re " *[@].+")))
|
||
(progn
|
||
(forward-line -1)
|
||
(end-of-line)
|
||
(setq end (point))
|
||
(setq cont nil)))
|
||
(if cont (setq cont (= (forward-line 1) 0))))
|
||
end)))
|
||
|
||
(defun ess-roxy-entry-p (&optional field)
|
||
"Non-nil if point is in a roxy entry.
|
||
FIELD allows checking for a specific field with
|
||
`ess-roxy-current-field'."
|
||
(and ess-roxy-mode
|
||
(save-excursion
|
||
(beginning-of-line)
|
||
(looking-at-p ess-roxy-re))
|
||
(or (null field)
|
||
(string= (ess-roxy-current-field) field))))
|
||
|
||
(defun ess-roxy-narrow-to-field ()
|
||
"Go to to the start of current field."
|
||
(interactive)
|
||
(let ((beg (ess-roxy-beg-of-field))
|
||
(end (ess-roxy-end-of-field)))
|
||
(narrow-to-region beg end)))
|
||
|
||
(defun ess-roxy-extract-field ()
|
||
(let ((field (buffer-substring (ess-roxy-beg-of-entry)
|
||
(ess-roxy-end-of-entry)))
|
||
(prefix-re (ess-roxy-guess-str))
|
||
(roxy-re ess-roxy-re))
|
||
(with-temp-buffer
|
||
(setq ess-roxy-re roxy-re)
|
||
(insert field)
|
||
(goto-char (point-min))
|
||
(while (re-search-forward prefix-re (point-max) 'noerror)
|
||
(replace-match ""))
|
||
(buffer-substring (point-min) (point-max)))))
|
||
|
||
(defun ess-roxy-adaptive-fill-function ()
|
||
"Return prefix for filling paragraph or nil if not determined."
|
||
(when (ess-roxy-entry-p)
|
||
(let ((roxy-str (car (split-string (ess-roxy-guess-str) "'"))))
|
||
(if (ess-roxy-in-header-p)
|
||
(save-excursion
|
||
(ess-back-to-roxy)
|
||
(re-search-forward "\\([ \t]*\\)" (line-end-position) t)
|
||
(concat roxy-str "' " (match-string 1)))
|
||
(concat roxy-str "' " (make-string ess-indent-offset ? ))))))
|
||
|
||
(defun ess-roxy-current-field ()
|
||
"Return the name of the field at point."
|
||
(and (not (ess-roxy-in-header-p))
|
||
(save-excursion
|
||
(goto-char (ess-roxy-beg-of-field))
|
||
(if (re-search-forward (concat ess-roxy-re
|
||
"[ \t]+@\\([[:alpha:]]+\\)")
|
||
(line-end-position) t)
|
||
(match-string-no-properties 1)))))
|
||
|
||
(defun ess-roxy-maybe-indent-line ()
|
||
"Indent line when point is in a field, but not in its first line."
|
||
(when (and (not (ess-roxy-in-header-p))
|
||
(not (equal (ess-roxy-current-field) "examples"))
|
||
(save-excursion
|
||
(beginning-of-line)
|
||
(let ((line-n (count-lines 1 (point))))
|
||
(goto-char (ess-roxy-beg-of-field))
|
||
(not (equal line-n (count-lines 1 (point)))))))
|
||
(ess-back-to-roxy)
|
||
(delete-region (point) (progn (skip-chars-forward " \t") (point)))
|
||
(insert (make-string ess-indent-offset ? ))))
|
||
|
||
(defun ess-roxy-goto-func-def ()
|
||
"Put point at start of function.
|
||
Go to the beginning of the current one or below the current
|
||
roxygen entry, error otherwise"
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(ess-roxy-goto-end-of-entry)
|
||
(forward-line 1)
|
||
(beginning-of-line))
|
||
(unless (looking-at-p ess-function-pattern)
|
||
(beginning-of-defun))))
|
||
|
||
(defun ess-roxy-get-args-list-from-def ()
|
||
"Get args list for current function."
|
||
(save-excursion
|
||
(ess-roxy-goto-func-def)
|
||
(let ((args (ess-roxy-get-function-args)))
|
||
(mapcar (lambda (x) (cons x '(""))) args))))
|
||
|
||
(defun ess-roxy-insert-args (args &optional here)
|
||
"Insert an ARGS list to the end of the current roxygen entry.
|
||
If HERE is supplied start inputting `here'. Finish at end of
|
||
line."
|
||
(let* ((roxy-str (ess-roxy-guess-str))
|
||
arg-des)
|
||
(if (and here (< 1 here))
|
||
(goto-char here)
|
||
(ess-roxy-goto-end-of-entry)
|
||
(beginning-of-line)
|
||
(when (not (looking-at-p "="))
|
||
(end-of-line)))
|
||
(while (stringp (caar args))
|
||
(setq arg-des (pop args))
|
||
(unless (string= (car arg-des) "")
|
||
(insert (concat "\n" roxy-str " @param " (car arg-des) " "))
|
||
(insert
|
||
(ess-replace-in-string (concat (car (cdr arg-des))) "\n"
|
||
(concat "\n" roxy-str)))
|
||
(when ess-roxy-fill-param-p
|
||
(fill-paragraph))))))
|
||
|
||
(defun ess-roxy-merge-args (fun ent)
|
||
"Take two args lists (alists) and return their union.
|
||
The result holds all keys from both FUN and ENT but no duplicates and
|
||
association from ent are preferred over entries from fun. Also,
|
||
drop entries from ent that are not in fun and are associated with
|
||
the empty string."
|
||
(let ((res-arg nil)
|
||
(arg-des))
|
||
(while (stringp (caar fun))
|
||
(setq arg-des (pop fun))
|
||
(if (assoc (car arg-des) ent)
|
||
(setq res-arg
|
||
(cons (cons (car arg-des) (cdr (assoc (car arg-des) ent))) res-arg))
|
||
(setq res-arg (cons (cons (car arg-des) '("")) res-arg))))
|
||
(while (stringp (caar ent))
|
||
(setq arg-des (pop ent))
|
||
(if (and (not (assoc (car arg-des) res-arg)) (not (string= (car (cdr arg-des)) "")))
|
||
(setq res-arg (cons (cons (car arg-des) (cdr arg-des)) res-arg))))
|
||
(nreverse res-arg)))
|
||
|
||
(defun ess-roxy-update-entry ()
|
||
"Update the entry at point or the entry above the current function.
|
||
Add a template empty roxygen documentation if no roxygen entry is
|
||
available. The template can be customized via the variable
|
||
`ess-roxy-template-alist'. The parameter descriptions can are
|
||
filled if `ess-roxy-fill-param-p' is non-nil."
|
||
(interactive)
|
||
(unless (derived-mode-p 'ess-r-mode)
|
||
(user-error "%s mode not yet supported" major-mode))
|
||
(save-excursion
|
||
(let* ((args-fun (ess-roxy-get-args-list-from-def))
|
||
(args-ent (ess-roxy-get-args-list-from-entry))
|
||
(args (ess-roxy-merge-args args-fun args-ent))
|
||
(roxy-str (ess-roxy-guess-str))
|
||
(line-break "")
|
||
template tag-def)
|
||
(ess-roxy-goto-func-def)
|
||
(when (not (= (forward-line -1) 0))
|
||
(insert "\n")
|
||
(forward-line -1))
|
||
(when (and (not (looking-at "^\n")) (not (ess-roxy-entry-p)))
|
||
(end-of-line)
|
||
(insert "\n"))
|
||
(if (ess-roxy-entry-p)
|
||
(ess-roxy-insert-args args (1- (ess-roxy-delete-args)))
|
||
(setq template (copy-sequence ess-roxy-template-alist))
|
||
(while (stringp (caar template))
|
||
(setq tag-def (pop template))
|
||
(if (string= (car tag-def) "param")
|
||
(ess-roxy-insert-args args (point))
|
||
(if (string= (car tag-def) "description")
|
||
(insert (concat line-break roxy-str " "
|
||
(cdr tag-def) "\n" roxy-str))
|
||
(if (string= (car tag-def) "details")
|
||
(insert (concat line-break roxy-str " " (cdr tag-def)))
|
||
(insert (concat line-break roxy-str " @"
|
||
(car tag-def) " " (cdr tag-def))))))
|
||
(setq line-break "\n"))))))
|
||
|
||
(defun ess-roxy-goto-end-of-entry ()
|
||
"Put point at the bottom of the current entry or above the function at point.
|
||
Return t if the point is left in a roxygen entry, otherwise nil.
|
||
Error if point is not in function or roxygen entry."
|
||
(when (not (ess-roxy-entry-p))
|
||
(beginning-of-defun)
|
||
(forward-line -1))
|
||
(if (ess-roxy-entry-p)
|
||
(progn (goto-char (ess-roxy-end-of-entry))
|
||
t)
|
||
(forward-line)
|
||
nil)
|
||
(ess-roxy-entry-p))
|
||
|
||
(defun ess-roxy-goto-beg-of-entry ()
|
||
"Put point at the top of the entry at point or above the function at point.
|
||
Return t if the point is left in a roxygen
|
||
entry, otherwise nil. Error if point is not in function or
|
||
roxygen entry."
|
||
(if (not (ess-roxy-entry-p))
|
||
(progn
|
||
(goto-char (nth 0 (end-of-defun)))
|
||
(forward-line -1)))
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(goto-char (ess-roxy-beg-of-entry))
|
||
t)
|
||
(forward-line) nil))
|
||
|
||
(defun ess-roxy-delete-args ()
|
||
"Remove all args from the entry at point or above the function at point.
|
||
Return 0 if no deletions were made other wise the point at where
|
||
the last deletion ended"
|
||
(save-excursion
|
||
(let* ((cont t)
|
||
(field-beg 0)
|
||
entry-beg entry-end field-end)
|
||
(ess-roxy-goto-end-of-entry)
|
||
(setq entry-beg (ess-roxy-beg-of-entry))
|
||
(setq entry-end (ess-roxy-end-of-entry))
|
||
(goto-char entry-end)
|
||
(beginning-of-line)
|
||
(while (and (<= entry-beg (point)) (> entry-beg 0) cont)
|
||
(if (looking-at
|
||
(concat ess-roxy-re " *@param"))
|
||
(progn
|
||
(setq field-beg (ess-roxy-beg-of-field))
|
||
(setq field-end (ess-roxy-end-of-field))
|
||
(delete-region field-beg (+ field-end 1))))
|
||
(setq cont nil)
|
||
(if (= (forward-line -1) 0)
|
||
(setq cont t)))
|
||
field-beg)))
|
||
|
||
(defun ess-roxy-get-args-list-from-entry ()
|
||
"Fill an args list from the entry above the function where the point is."
|
||
(save-excursion
|
||
(let* (args entry-beg field-beg field-end args-text arg-name desc)
|
||
(if (ess-roxy-goto-end-of-entry)
|
||
(progn
|
||
(setq roxy-str (ess-roxy-guess-str))
|
||
(beginning-of-line)
|
||
(setq entry-beg (ess-roxy-beg-of-entry))
|
||
(while (and (< entry-beg (point)) (> entry-beg 0))
|
||
(if (looking-at
|
||
(concat ess-roxy-re " *@param"))
|
||
(progn
|
||
(setq field-beg (ess-roxy-beg-of-field))
|
||
(setq field-end (ess-roxy-end-of-field))
|
||
(setq args-text (buffer-substring-no-properties
|
||
field-beg field-end))
|
||
(setq args-text
|
||
(ess-replace-in-string args-text roxy-str ""))
|
||
(setq args-text
|
||
(ess-replace-in-string
|
||
args-text "[[:space:]]*@param *" ""))
|
||
;; (setq args-text
|
||
;; (ess-replace-in-string args-text "\n" ""))
|
||
(string-match "[^[:space:]]*" args-text)
|
||
(setq arg-name (match-string 0 args-text))
|
||
(setq desc (replace-regexp-in-string
|
||
(concat "^" (regexp-quote arg-name) " *") "" args-text))
|
||
(setq args (cons (list (concat arg-name)
|
||
(concat desc))
|
||
args))))
|
||
(forward-line -1))
|
||
args)
|
||
nil))))
|
||
|
||
(defun ess-roxy-toggle-roxy-region (beg end)
|
||
"Toggle prefix roxygen string from BEG to END.
|
||
Add the prefix if missing, remove if found. BEG and END default
|
||
to the region, if active, and otherwise the entire line. This is
|
||
convenient for editing example fields."
|
||
(interactive "r")
|
||
(unless (and beg end)
|
||
(setq beg (line-beginning-position)
|
||
end (line-end-position)))
|
||
(ess-roxy-roxy-region beg end (ess-roxy-entry-p)))
|
||
|
||
(defun ess-roxy-roxy-region (beg end &optional on)
|
||
(save-excursion
|
||
(let (RE to-string
|
||
(roxy-str (ess-roxy-guess-str)))
|
||
(narrow-to-region beg (- end 1))
|
||
(if on
|
||
(progn (setq RE (concat ess-roxy-re " +?"))
|
||
(setq to-string ""))
|
||
(setq RE "^")
|
||
(setq to-string (concat roxy-str " ")))
|
||
(goto-char beg)
|
||
(while (re-search-forward RE (point-max) 'noerror)
|
||
(replace-match to-string))
|
||
(widen))))
|
||
|
||
(defun ess-roxy-preview ()
|
||
"Generate documentation for roxygen entry at point.
|
||
Use a connected R session (starting one if necessary) and
|
||
`ess-roxy-package' to generate the Rd code for the entry at
|
||
point. Place it in a buffer and return that buffer."
|
||
(unless (derived-mode-p 'ess-r-mode)
|
||
(user-error "Preview only supported in R buffers, try `ess-r-devtools-document-package' instead"))
|
||
(let* ((beg (ess-roxy-beg-of-entry))
|
||
(tmpf (make-temp-file "ess-roxy"))
|
||
(roxy-buf (get-buffer-create " *RoxygenPreview*"))
|
||
(R-old-roxy
|
||
(concat
|
||
"..results <- roxygen2:::roc_process(rd_roclet(), parse.files(P), \"\");"
|
||
"cat(vapply(..results, function(x) roxygen2:::rd_out_cache$compute(x, format(x)), character(1)), \"\\n\")" ))
|
||
(R-new-roxy
|
||
(concat
|
||
"..results <- roc_proc_text(rd_roclet(), readChar(P, file.info(P)$size));"
|
||
"cat(vapply(..results, format, character(1)), \"\\n\")" ))
|
||
(out-rd-roclet
|
||
(cond ((string= "roxygen" ess-roxy-package)
|
||
"make.Rd2.roclet()$parse")
|
||
;; must not line break strings to avoid getting +s in the output
|
||
((string= "roxygen2" ess-roxy-package)
|
||
(concat "(function(P) { if(packageVersion('roxygen2') < '3.0.0') {"
|
||
R-old-roxy "} else {" R-new-roxy "} })"))
|
||
(t (error "Need to hard code the roclet output call for roxygen package '%s'"
|
||
ess-roxy-package)))))
|
||
(when (= beg 0)
|
||
(error "Point is not in a Roxygen entry"))
|
||
(save-excursion
|
||
(goto-char (ess-roxy-end-of-entry))
|
||
(forward-line 1)
|
||
(if (end-of-defun)
|
||
(append-to-file beg (point) tmpf)
|
||
(while (and (forward-line 1)
|
||
(not (looking-at-p "^$"))
|
||
(not (eobp))
|
||
(not (looking-at-p ess-roxy-re))))
|
||
(append-to-file beg (point) tmpf))
|
||
(inferior-ess-r-force)
|
||
(ess-force-buffer-current)
|
||
(unless (ess-boolean-command (concat "print(suppressWarnings(require(" ess-roxy-package
|
||
", quietly=TRUE)))\n"))
|
||
(error (concat "Failed to load the " ess-roxy-package " package; "
|
||
"in R, try install.packages(\"" ess-roxy-package "\")")))
|
||
(ess-command (concat out-rd-roclet "(\"" tmpf "\")\n") roxy-buf)
|
||
(with-current-buffer roxy-buf
|
||
;; Kill characters up to % in case we missed stripping prompts
|
||
;; or +'s:
|
||
(goto-char (point-min))
|
||
(when (re-search-forward "%" (line-end-position) t)
|
||
(backward-char)
|
||
(delete-region (line-beginning-position) (point)))))
|
||
(delete-file tmpf)
|
||
roxy-buf))
|
||
|
||
(defun ess-roxy-preview-HTML (&optional visit-instead-of-browse)
|
||
"Use a (possibly newly) connected R session and the roxygen package to
|
||
generate a HTML page for the roxygen entry at point and open that
|
||
buffer in a browser. Visit the HTML file instead of showing it in
|
||
a browser if `visit-instead-of-browse' is non-nil."
|
||
(interactive "P")
|
||
(let* ((roxy-buf (ess-roxy-preview))
|
||
(rd-tmp-file (make-temp-file "ess-roxy-" nil ".Rd"))
|
||
(html-tmp-file (make-temp-file "ess-roxy-" nil ".html"))
|
||
(rd-to-html (concat "Rd2HTML(\"" rd-tmp-file "\",\""
|
||
html-tmp-file "\", stages=c(\"render\"))"))
|
||
)
|
||
(with-current-buffer roxy-buf
|
||
(set-visited-file-name rd-tmp-file)
|
||
(save-buffer)
|
||
(kill-buffer roxy-buf))
|
||
(ess-force-buffer-current)
|
||
(ess-command "print(suppressWarnings(require(tools, quietly=TRUE)))\n")
|
||
(if visit-instead-of-browse
|
||
(progn
|
||
(ess-command (concat rd-to-html "\n"))
|
||
(find-file html-tmp-file))
|
||
(ess-command (concat "browseURL(" rd-to-html ")\n")))))
|
||
|
||
(defun ess-roxy-preview-text ()
|
||
"Use the connected R session and the roxygen package to
|
||
generate the text help page of the roxygen entry at point."
|
||
(interactive)
|
||
(with-current-buffer (ess-roxy-preview)
|
||
(Rd-preview-help)))
|
||
|
||
(defun ess-roxy-preview-Rd (&optional name-file)
|
||
"Preview Rd for the roxygen entry at point.
|
||
Use the connected R session and the roxygen package to
|
||
generate the Rd code for the roxygen entry at point. If called
|
||
with a non-nil NAME-FILE (\\[universal-argument]),
|
||
also set the visited file name of the created buffer to
|
||
facilitate saving that file."
|
||
(interactive "P")
|
||
(let ((roxy-buf (ess-roxy-preview)))
|
||
(pop-to-buffer roxy-buf)
|
||
(if name-file
|
||
(save-excursion
|
||
(goto-char 1)
|
||
(search-forward-regexp "name{\\(.+\\)}")
|
||
(set-visited-file-name (concat (match-string 1) ".Rd"))))
|
||
(Rd-mode)
|
||
;; why should the following be needed here? [[currently has no effect !!]]
|
||
;; usually in a *.Rd file fontification happens automatically
|
||
(font-lock-ensure)))
|
||
|
||
|
||
(defun ess-roxy-guess-str (&optional not-here)
|
||
"Guess the prefix used in the current roxygen block.
|
||
If NOT-HERE is non-nil, guess the prefix for nearest roxygen
|
||
block before the point."
|
||
(save-excursion
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(goto-char (line-beginning-position))
|
||
(search-forward-regexp ess-roxy-re))
|
||
(if not-here
|
||
(search-backward-regexp ess-roxy-re)))
|
||
(if (or not-here (ess-roxy-entry-p))
|
||
(match-string 0)
|
||
(if (derived-mode-p 'ess-r-mode)
|
||
ess-roxy-str
|
||
(concat (string-trim comment-start) "'")))))
|
||
|
||
(defun ess-roxy-hide-block ()
|
||
"Hide current roxygen comment block."
|
||
(interactive)
|
||
(save-excursion
|
||
(let ((end-of-entry (ess-roxy-end-of-entry))
|
||
(beg-of-entry (ess-roxy-beg-of-entry)))
|
||
(hs-hide-block-at-point nil (list beg-of-entry end-of-entry)))))
|
||
|
||
(defun ess-roxy-toggle-hiding ()
|
||
"Toggle hiding/showing of a block.
|
||
See `hs-show-block' and `ess-roxy-hide-block'."
|
||
(interactive)
|
||
(hs-life-goes-on
|
||
(if (hs-overlay-at (line-end-position))
|
||
(hs-show-block)
|
||
(ess-roxy-hide-block))))
|
||
|
||
(defun ess-roxy-show-all ()
|
||
"Hide all Roxygen entries in current buffer."
|
||
(interactive)
|
||
(ess-roxy-hide-all t))
|
||
|
||
(defun ess-roxy-hide-all (&optional show)
|
||
"Hide all Roxygen entries in current buffer."
|
||
(interactive)
|
||
(when (not ess-roxy-hide-show-p)
|
||
(user-error "First enable hide-show with `ess-roxy-hide-show-p'"))
|
||
(hs-life-goes-on
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(while (re-search-forward (concat ess-roxy-re) (point-max) t 1)
|
||
(let ((end-of-entry (ess-roxy-end-of-entry)))
|
||
(if show
|
||
(hs-show-block)
|
||
(ess-roxy-hide-block))
|
||
(goto-char end-of-entry)
|
||
(forward-line 1))))))
|
||
|
||
(defun ess-roxy-previous-entry ()
|
||
"Go to beginning of previous Roxygen entry."
|
||
(interactive)
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(goto-char (ess-roxy-beg-of-entry))
|
||
(forward-line -1)))
|
||
(search-backward-regexp ess-roxy-re (point-min) t 1)
|
||
(goto-char (ess-roxy-beg-of-entry)))
|
||
|
||
(defun ess-roxy-next-entry ()
|
||
"Go to beginning of next Roxygen entry."
|
||
(interactive)
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(goto-char (ess-roxy-end-of-entry))
|
||
(forward-line 1)))
|
||
(search-forward-regexp ess-roxy-re (point-max) t 1)
|
||
(goto-char (ess-roxy-beg-of-entry)))
|
||
|
||
(defun ess-roxy-get-function-args ()
|
||
"Return the arguments specified for the current function as a list of strings.
|
||
Assumes point is at the beginning of the function."
|
||
(save-excursion
|
||
(let ((args-txt
|
||
(buffer-substring-no-properties
|
||
(progn
|
||
(search-forward-regexp "\\([=,-]+ *function *\\|^\s*function\\)" nil nil 1)
|
||
(+ (point) 1))
|
||
(progn
|
||
(ess-roxy-match-paren)
|
||
(point)))))
|
||
(setq args-txt (replace-regexp-in-string "#+[^\"']*\n" "" args-txt))
|
||
(setq args-txt (replace-regexp-in-string "([^)]+)" "" args-txt))
|
||
(setq args-txt (replace-regexp-in-string "=[^,]+" "" args-txt))
|
||
(setq args-txt (replace-regexp-in-string "[ \t\n]+" "" args-txt))
|
||
(split-string args-txt ","))))
|
||
|
||
(defun ess-roxy-match-paren ()
|
||
"Go to the matching parenthesis."
|
||
(cond ((looking-at "\\s\(") (forward-list 1) (backward-char 1))
|
||
((looking-at "\\s\)") (forward-char 1) (backward-list 1))))
|
||
|
||
(defun ess-roxy-complete-tag ()
|
||
"Complete the tag at point."
|
||
(let ((bounds (ess-bounds-of-symbol)))
|
||
(when (and bounds
|
||
(save-excursion
|
||
(goto-char (car bounds))
|
||
(eq (following-char) ?@)))
|
||
(list (1+ (car bounds)) (cdr bounds)
|
||
(append ess-roxy-tags-noparam ess-roxy-tags-param)))))
|
||
|
||
(defun ess-roxy-tag-completion ()
|
||
"Completion data for Emacs >= 24."
|
||
(when (save-excursion (re-search-backward "@\\<\\(\\w*\\)" (line-beginning-position) t))
|
||
(let ((beg (match-beginning 1))
|
||
(end (match-end 1)))
|
||
(when (and end (= end (point)))
|
||
(list beg end (append ess-roxy-tags-noparam ess-roxy-tags-param) :exclusive 'no)))))
|
||
|
||
(defun ess-roxy-remove-roxy-re (string)
|
||
"Remove `ess-roxy-str' from STRING before sending to R process.
|
||
Useful for sending code from example section. This function is
|
||
placed in `ess-presend-filter-functions'."
|
||
;; In the future we might want to detect chunks between markdown
|
||
;; fences and strip everything that comes before `@examples`
|
||
(if (ess-roxy--all-prefixed string)
|
||
(replace-regexp-in-string (concat ess-roxy-re "\\s-*") "" string)
|
||
string))
|
||
|
||
(defun ess-roxy--all-prefixed (string)
|
||
(let ((ess-roxy-re-lexical ess-roxy-re))
|
||
(with-temp-buffer
|
||
(insert string)
|
||
(goto-char 0)
|
||
(while (and (looking-at-p ess-roxy-re-lexical)
|
||
(re-search-forward "\n" nil t)))
|
||
(looking-at-p ess-roxy-re-lexical))))
|
||
|
||
(defun ess-roxy-find-par-end (stop-point &rest stoppers)
|
||
(mapc #'(lambda (stopper)
|
||
(when (and (> stop-point (point))
|
||
(save-excursion
|
||
(re-search-forward stopper stop-point t)))
|
||
(setq stop-point (match-beginning 0))))
|
||
stoppers)
|
||
(save-excursion
|
||
(goto-char stop-point)
|
||
(line-end-position 0)))
|
||
|
||
|
||
;;*;; Advices
|
||
(defmacro ess-roxy-with-filling-context (examples &rest body)
|
||
"Setup context (e.g. `comment-start') for filling roxygen BODY.
|
||
EXAMPLES should be non-nil if filling an example block."
|
||
(declare (indent 2) (debug (&rest form)))
|
||
`(let ((comment-start (concat ess-roxy-re "[ \t]+#"))
|
||
(comment-start-skip (concat ess-roxy-re "[ \t]+# *"))
|
||
(comment-use-syntax nil)
|
||
(adaptive-fill-first-line-regexp (concat ess-roxy-re "[ \t]*"))
|
||
(paragraph-start (concat "\\(" ess-roxy-re "\\(" paragraph-start
|
||
"\\|[ \t]*@" "\\)" "\\)\\|\\(" paragraph-start "\\)"))
|
||
(temp-table (if ,examples
|
||
(make-syntax-table ess-r-mode-syntax-table)
|
||
Rd-mode-syntax-table)))
|
||
(when ,examples
|
||
;; Prevent the roxy prefix to be interpreted as comment or string
|
||
;; starter
|
||
(modify-syntax-entry ?# "w" temp-table)
|
||
(modify-syntax-entry ?' "w" temp-table))
|
||
;; Neutralize (comment-normalize-vars) because it modifies the
|
||
;; comment-start regexp in such a way that paragraph filling of
|
||
;; comments in @examples fields does not work
|
||
(cl-letf (((symbol-function 'comment-normalize-vars) #'ignore))
|
||
(with-syntax-table temp-table
|
||
,@body))))
|
||
|
||
(defun ess-roxy-ess-indent-command (&optional whole-exp)
|
||
"Hide this block if we are at the beginning of the line.
|
||
Else call `ess-indent-command'."
|
||
(interactive "P")
|
||
(if (and (bolp) (ess-roxy-entry-p) ess-roxy-hide-show-p)
|
||
(progn (ess-roxy-toggle-hiding))
|
||
(ess-indent-command whole-exp)))
|
||
|
||
(defun ess--roxy-fill-block (fun &optional args)
|
||
"Fill a roxygen block.
|
||
FUN should be a filling function and ARGS gets passed to it."
|
||
(let* ((saved-pos (point))
|
||
(par-start (save-excursion
|
||
(if (save-excursion
|
||
(and (backward-paragraph)
|
||
(forward-paragraph)
|
||
(<= (point) saved-pos)))
|
||
(line-beginning-position)
|
||
(progn (backward-paragraph) (point)))))
|
||
(par-end (ess-roxy-find-par-end
|
||
(save-excursion
|
||
(forward-paragraph)
|
||
(point))
|
||
(concat ess-roxy-re "[ \t]*@examples\\b") "^[^#]")))
|
||
;; Refill the whole structural paragraph sequentially, field by
|
||
;; field, stopping at @examples
|
||
(ess-roxy-with-filling-context nil
|
||
(save-excursion
|
||
(save-restriction
|
||
(narrow-to-region par-start par-end)
|
||
(goto-char (point-min))
|
||
(while (< (point) (point-max))
|
||
(ess-roxy-maybe-indent-line)
|
||
(apply fun args)
|
||
(forward-paragraph)))))))
|
||
|
||
(defun ess-r--fill-paragraph (orig-fun &rest args)
|
||
"ESS fill paragraph for R mode.
|
||
Overrides `fill-paragraph' which is ORIG-FUN when necessary and
|
||
passes ARGS to it."
|
||
(cond
|
||
;; Regular case
|
||
((not (derived-mode-p 'ess-r-mode))
|
||
(apply orig-fun args))
|
||
;; Filling of code comments in @examples roxy field
|
||
((and (ess-roxy-entry-p)
|
||
(save-excursion
|
||
(ess-roxy-goto-end-of-roxy-comment)
|
||
(looking-at "#")))
|
||
(ess-roxy-with-filling-context t
|
||
(apply orig-fun args)))
|
||
((and (not (ess-roxy-entry-p))
|
||
(ess-inside-comment-p))
|
||
(apply orig-fun args))
|
||
;; Filling of call arguments with point on call name
|
||
((and ess-fill-calls
|
||
(ess-inside-call-name-p))
|
||
(save-excursion
|
||
(skip-chars-forward "^([")
|
||
(forward-char)
|
||
(ess-fill-args)))
|
||
;; Filling of continuations
|
||
((and ess-fill-continuations
|
||
(ess-inside-continuation-p))
|
||
(ess-fill-continuations))
|
||
;; Filling of call arguments
|
||
((and ess-fill-calls
|
||
(ess-inside-call-p))
|
||
(ess-fill-args))
|
||
;; Filling of roxy blocks
|
||
((ess-roxy-entry-p)
|
||
(ess--roxy-fill-block orig-fun args))
|
||
(t
|
||
(apply orig-fun args))))
|
||
(advice-add 'fill-paragraph :around #'ess-r--fill-paragraph)
|
||
|
||
(defun ess-roxy-move-beginning-of-line (arg)
|
||
"Move point to the beginning of the current line or roxygen comment.
|
||
If not in a roxygen comment, call `move-beginning-of-line', which
|
||
see for ARG. If in a roxygen field, leave point at the end of a
|
||
roxygen comment. If already there, move to the beginning of the
|
||
line."
|
||
(interactive "^p")
|
||
(if (ess-roxy-entry-p)
|
||
(let ((pos (point)))
|
||
(ess-roxy-goto-end-of-roxy-comment)
|
||
(when (eql (point) pos)
|
||
(move-beginning-of-line nil)))
|
||
(move-beginning-of-line arg)))
|
||
|
||
(defun ess-roxy-goto-end-of-roxy-comment ()
|
||
"Leave point at the end of a roxygen comment.
|
||
If not in a roxygen entry, call `back-to-indentation'."
|
||
(interactive)
|
||
(if (ess-roxy-entry-p)
|
||
(progn
|
||
(end-of-line)
|
||
(re-search-backward (concat ess-roxy-re " *")
|
||
(line-beginning-position) t)
|
||
(goto-char (match-end 0)))
|
||
(back-to-indentation)))
|
||
|
||
(defun ess-roxy-indent-new-comment-line ()
|
||
(if (not (ess-roxy-entry-p))
|
||
(indent-new-comment-line)
|
||
(ess-roxy-indent-on-newline)))
|
||
|
||
(define-obsolete-function-alias 'ess-roxy-newline-and-indent
|
||
#'ess-roxy-newline "ESS 19.04")
|
||
(defun ess-roxy-newline ()
|
||
"Start a newline and insert the roxygen prefix.
|
||
Only do this if in a roxygen block and
|
||
`ess-roxy-insert-prefix-on-newline' is non-nil."
|
||
(interactive)
|
||
(if (and (ess-roxy-entry-p)
|
||
ess-roxy-insert-prefix-on-newline)
|
||
(ess-roxy-indent-on-newline)
|
||
(newline nil t)))
|
||
|
||
(defun ess-roxy-indent-on-newline ()
|
||
"Insert a newline in a roxygen field."
|
||
(cond
|
||
;; Point at beginning of first line of entry; do nothing
|
||
((= (point) (ess-roxy-beg-of-entry))
|
||
(newline-and-indent))
|
||
;; Otherwise: skip over roxy comment string if necessary and then
|
||
;; newline and then inset new roxy comment string
|
||
(t
|
||
(let ((point-after-roxy-string
|
||
(save-excursion (forward-line 0)
|
||
(ess-back-to-roxy)
|
||
(point))))
|
||
(goto-char (max (point) point-after-roxy-string)))
|
||
(newline-and-indent)
|
||
(insert (concat (ess-roxy-guess-str t) " ")))))
|
||
|
||
(defun ess-roxy-cpp-fill-paragraph (&rest _args)
|
||
"Advice for `c-fill-paragraph' that accounts for roxygen comments."
|
||
(cond
|
||
;; Fill roxy @example's.
|
||
((ess-roxy-entry-p "examples")
|
||
(ess--roxy-fill-block 'fill-paragraph) nil)
|
||
;; Fill roxy entries.
|
||
((ess-roxy-entry-p)
|
||
(ess--roxy-fill-block 'fill-paragraph) nil)
|
||
;; Return t to signal to go on to `c-fill-paragraph'.
|
||
(t t)))
|
||
|
||
(advice-add 'c-fill-paragraph :before-while #'ess-roxy-cpp-fill-paragraph)
|
||
|
||
(defun ess-roxy-enable-in-cpp ()
|
||
"Enable `ess-roxy-mode' in C++ buffers in R packages."
|
||
(when (and (fboundp 'ess-r-package-project)
|
||
(ess-r-package-project))
|
||
(ess-roxy-mode)))
|
||
|
||
(with-eval-after-load "cc-mode"
|
||
(add-hook 'c++-mode-hook #'ess-roxy-enable-in-cpp))
|
||
|
||
(defun ess-roxy--region-p (beg end)
|
||
(when ess-roxy-re
|
||
(save-excursion
|
||
(goto-char beg)
|
||
(catch 'ess-r-not-roxy
|
||
(while (< (point) end)
|
||
(unless (looking-at-p ess-roxy-re)
|
||
(throw 'ess-r-not-roxy nil))
|
||
(forward-line))
|
||
t))))
|
||
|
||
(provide 'ess-roxy)
|
||
|
||
;;; ess-roxy.el ends here
|