;;; ess-roxy.el --- convenient editing of in-code roxygen documentation
|
|
;;
|
|
;; Copyright (C) 2009--2018 Henning Redestig, A.J. Rossini, Richard
|
|
;; M. Heiberger, Martin Maechler, Kurt Hornik, Rodney Sparapani, Stephen
|
|
;; Eglen, Vitalie Spinu, and J. Alexander Branham.
|
|
;;
|
|
;; Author: Henning Redestig <henning.red * go0glemail c-m>
|
|
;; Keywords: convenience, tools
|
|
;;
|
|
;; This file is part of ESS
|
|
;;
|
|
;; 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
|
|
;; <https://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-and-indent)
|
|
(define-key map [remap newline-and-indent] #'ess-roxy-newline-and-indent)
|
|
(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-overwiew))
|
|
(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)))))
|
|
|
|
(when (featurep 'outline-magic)
|
|
(substitute-key-definition 'outline-cyle
|
|
'ess-roxy-cyle-example
|
|
ess-roxy-mode-map outline-mode-menu-bar-map))
|
|
|
|
(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 " ?") (point-at-bol))
|
|
(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)))" ))
|
|
(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 (point-at-bol))
|
|
(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 (point-at-eol))
|
|
(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*\\)" (point-at-bol) 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'."
|
|
;; Only strip the prefix in the @examples field, and only when
|
|
;; STRING is entirely contained inside it. This allows better
|
|
;; behavior for evaluation of regions.
|
|
(let ((roxy-re ess-roxy-re))
|
|
(if (and (ess-roxy-entry-p "examples")
|
|
;; don't send just @examples if we're looking at a line
|
|
;; like: ##' @examples
|
|
(not (string-match-p (concat roxy-re "[[:space:]]*@") string))
|
|
;; (with-temp-buffer
|
|
;; ;; Need to carry the buffer-local value of
|
|
;; ;; `ess-roxy-re' into the temp buffer:
|
|
;; (setq ess-roxy-re roxy-re)
|
|
;; (insert string)
|
|
;; (ess-roxy-entry-p))
|
|
)
|
|
(replace-regexp-in-string (concat ess-roxy-re "\\s-*") "" string)
|
|
string)))
|
|
|
|
(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 " *") (point-at-bol) 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)))
|
|
|
|
(defun ess-roxy-newline-and-indent ()
|
|
"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-and-indent)))
|
|
|
|
(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))
|
|
|
|
(provide 'ess-roxy)
|
|
|
|
;;; ess-roxy.el ends here
|