|
|
- ;;; ess-r-completion.el --- R completion -*- lexical-binding: t; -*-
- ;;
- ;; Copyright (C) 2015 A.J. Rossini, Richard M. Heiberger, Martin Maechler, Kurt
- ;; Hornik, Rodney Sparapani, Stephen Eglen and Vitalie Spinu.
- ;;
- ;; Author: Vitalie Spinu
- ;; Maintainer: ESS-core <ESS-core@r-project.org>
- ;;
- ;; Keywords: languages, statistics
- ;;
- ;; This file is part of ESS.
- ;;
- ;; This file 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 2, or (at your option)
- ;; any later version.
- ;;
- ;; This file 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.
- ;;
- ;; A copy of the GNU General Public License is available at
- ;; https://www.r-project.org/Licenses/
- ;;
- ;;; Commentary:
- ;;
- ;;; Code:
-
- ;;; ElDoc
-
- (require 'cl-lib)
- (require 'ess-inf)
- (require 'ess-help)
-
- (eval-when-compile
- (require 'subr-x))
-
- (defvar ac-auto-start)
- (defvar ac-prefix)
- (defvar ac-point)
- (defvar ac-use-comphist)
- (declare-function company-begin-backend "company")
- (declare-function company-doc-buffer "company")
-
- (defcustom ess-R-argument-suffix " = "
- "Suffix appended by `ac-source-R' and `ac-source-R-args' to candidates."
- :group 'ess-R
- :type 'string)
-
- (defun ess-r-eldoc-function ()
- "Return the doc string, or nil.
- If an ESS process is not associated with the buffer, do not try
- to look up any doc strings."
- (when (and eldoc-mode ess-can-eval-in-background)
- (let* ((proc (ess-get-next-available-process))
- (funname (and proc (or (and ess-eldoc-show-on-symbol ;; Aggressive completion
- (thing-at-point 'symbol))
- (car (ess--fn-name-start))))))
- (when funname
- (let* ((args (ess-function-arguments funname proc))
- (bargs (cadr args))
- (doc (mapconcat (lambda (el)
- (if (equal (car el) "...")
- "..."
- (concat (car el) "=" (cdr el))))
- bargs ", "))
- (margs (nth 2 args))
- (W (- (window-width (minibuffer-window)) (+ 4 (length funname))))
- doc1)
- (when doc
- (setq doc (ess-eldoc-docstring-format funname doc))
- (when (and margs (< (length doc1) W))
- (setq doc1 (concat doc (propertize " || " 'face font-lock-function-name-face)))
- (while (and margs (< (length doc1) W))
- (let ((head (pop margs)))
- (unless (assoc head bargs)
- (setq doc doc1
- doc1 (concat doc1 head "=, ")))))
- (when (equal (substring doc -2) ", ")
- (setq doc (substring doc 0 -2)))
- (when (and margs (< (length doc) W))
- (setq doc (concat doc " {--}"))))
- doc))))))
-
- (defun ess-eldoc-docstring-format (funname doc)
- (save-match-data
- (let* (;; (name (symbol-name sym))
- (truncate (or (not (eq t eldoc-echo-area-use-multiline-p))
- (eq ess-eldoc-abbreviation-style 'aggressive)))
- ;; Subtract 1 from window width since will cause a wraparound and
- ;; resize of the echo area.
- (W (1- (- (window-width (minibuffer-window))
- (+ 2 (length funname))))))
- (setq doc
- (if (or (<= (length doc) W)
- (null ess-eldoc-abbreviation-style)
- (eq 'none ess-eldoc-abbreviation-style))
- doc
- ;;MILD filter
- (setq doc (replace-regexp-in-string "TRUE" "T" doc))
- (setq doc (replace-regexp-in-string "FALSE" "F" doc))
- (if (or (<= (length doc) W)
- (eq 'mild ess-eldoc-abbreviation-style))
- doc
- ;;NORMAL filter (deal with long defaults)
- (setq doc (replace-regexp-in-string
- ;; function calls inside default docs foo(xxxx{..})
- "([^)]\\{8\\}\\([^)]\\{4,\\}\\))"
- "{.}" doc nil nil 1))
- (if (<= (length doc) W)
- doc
- (setq doc (replace-regexp-in-string
- " +[^ \t=,\"\]+=[^ \t]\\{10\\}\\([^ \t]\\{4,\\}\\)\\(,\\|\\'\\)"
- "{.}," doc nil nil 1))
- (if (<= (length doc) W)
- doc
- (setq doc (replace-regexp-in-string
- " +[^ \t=,\"]+=\\([^ \t]\\{10,\\}\\)\\(,\\|\\'\\)"
- "{.}," doc nil nil 1))
- (if (or (<= (length doc) W)
- (eq 'normal ess-eldoc-abbreviation-style))
- doc
- ;;STRONG filter (replace defaults)
- (setq doc (replace-regexp-in-string
- " *[^ \t=,\"\\]* = \\([^ \t]\\{4,\\}\\)\\(,\\|\\'\\)"
- "{.}," doc nil nil 1))
- (if (<= (length doc) W)
- doc
- (setq doc (replace-regexp-in-string
- "\\(=[^FT0-9].+?\\)\\(, [^ =,\"\\]+=\\|\\'\\)"
- "" doc nil nil 1))
- (setq doc (replace-regexp-in-string
- "\\(=[^FT0-9].+?\\)\\(, [^ =,\"\\]+,\\|\\'\\)"
- "" doc nil nil 1))
- (if (or (<= (length doc) W)
- (eq 'strong ess-eldoc-abbreviation-style))
- doc
- ;;AGGRESSIVE filter (truncate what is left)
- (concat (substring doc 0 (- W 4)) "{--}")))))))))
- (when (and truncate
- (> (length doc) W))
- (setq doc (concat (substring doc 0 (- W 4)) "{--}")))
- (format "%s: %s" (propertize funname 'face 'font-lock-function-name-face) doc))))
-
- ;;; OBJECTS
-
- (defun ess-r-object-completion ()
- "Return completions at point in a format required by `completion-at-point-functions'."
- (if (ess-make-buffer-current)
- (let* ((funstart (cdr (ess--fn-name-start)))
- (completions (ess-r-get-rcompletions funstart))
- (token (pop completions)))
- (when completions
- (list (- (point) (length token)) (point)
- completions)))
- (when (string-match "complete" (symbol-name last-command))
- (message "No ESS process associated with current buffer")
- nil)))
-
- (defun ess-complete-object-name ()
- "Perform completion on object preceding point.
- Uses `ess-r-complete-object-name' when `ess-use-R-completion' is non-nil,
- and `ess-internal-complete-object-name' otherwise."
- (interactive)
- (if (ess-make-buffer-current)
- (if ess-use-R-completion
- (ess-r-complete-object-name)
- (ess-internal-complete-object-name))
- ;; else give a message on second invocation
- (when (string-match "complete" (symbol-name last-command))
- (message "No ESS process associated with current buffer")
- nil)))
-
- (define-obsolete-function-alias 'ess-list-object-completions #'ess-complete-object-name "ESS 19.04")
-
- ;; This one is needed for R <= 2.6.x -- hence *not* obsoleting it
- (defun ess-internal-complete-object-name ()
- "Perform completion on `ess-language' object preceding point.
- The object is compared against those objects known by
- `ess-get-object-list' and any additional characters up to ambiguity are
- inserted. Completion only works on globally-known objects (including
- elements of attached data frames), and thus is most suitable for
- interactive `command-line' entry, and not so much for function editing
- since local objects (e.g. argument names) aren't known.
-
- Use \\[ess-resynch] to re-read the names of the attached directories.
- This is done automatically (and transparently) if a directory is
- modified (S only!), so the most up-to-date list of object names is always
- available. However attached dataframes are *not* updated, so this
- command may be necessary if you modify an attached dataframe."
- (ess-make-buffer-current)
- (if (memq (char-syntax (preceding-char)) '(?w ?_))
- (let* ((comint-completion-addsuffix nil)
- (bounds (ess-bounds-of-symbol))
- (beg (car bounds))
- (end (cdr bounds))
- (full-prefix (buffer-substring beg end))
- (pattern full-prefix)
- ;; See if we're indexing a list with `$'
- (listname (if (string-match "\\(.+\\)\\$\\(\\(\\sw\\|\\s_\\)*\\)$"
- full-prefix)
- (progn
- (setq pattern
- (if (not (match-beginning 2)) ""
- (substring full-prefix
- (match-beginning 2)
- (match-end 2))))
- (substring full-prefix (match-beginning 1)
- (match-end 1)))))
- ;; are we trying to get a slot via `@' ?
- (classname (if (string-match "\\(.+\\)@\\(\\(\\sw\\|\\s_\\)*\\)$"
- full-prefix)
- (progn
- (setq pattern
- (if (not (match-beginning 2)) ""
- (substring full-prefix
- (match-beginning 2)
- (match-end 2))))
- (ess-write-to-dribble-buffer
- (format "(ess-C-O-Name : slots..) : patt=%s"
- pattern))
- (substring full-prefix (match-beginning 1)
- (match-end 1)))))
- (components (if listname
- (ess-object-names listname)
- (if classname
- (ess-slot-names classname)
- ;; Default case: It hangs here when
- ;; options(error=recover) :
- (ess-get-object-list ess-current-process-name)))))
- ;; always return a non-nil value to prevent history expansions
- (or (completion-in-region beg end components) 'none))))
-
- (defun ess-r-get-rcompletions (&optional start end prefix allow-3-dots)
- "Call R internal completion utilities (rcomp) for possible completions.
- Optional START and END delimit the entity to complete, default to
- bol and point. If PREFIX is given, perform completion on
- PREFIX. First element of the returned list is the completion
- token. Needs version of R >= 2.7.0."
- (let* ((start (or start
- (if prefix
- 0
- (save-excursion (comint-bol nil) (point)))))
- (end (or end (if prefix (length prefix) (point))))
- (prefix (or prefix (buffer-substring start end)))
- ;; (opts1 (if no-args "op<-rc.options(args=FALSE)" ""))
- ;; (opts2 (if no-args "rc.options(op)" ""))
- (call1 (format ".ess_get_completions(\"%s\", %d, \"%s\")"
- (ess-quote-special-chars prefix)
- (- end start)
- ess-R-argument-suffix))
- (cmd (if allow-3-dots
- (concat call1 "\n")
- (concat "local({ r <- " call1 "; r[r != '...='] })\n"))))
- (ess-get-words-from-vector cmd)))
-
- (defun ess-r-complete-object-name ()
- "Completion in R via R's completion utilities (formerly 'rcompgen').
- To be used instead of ESS' completion engine for R versions >= 2.7.0."
- (let ((possible-completions (ess-r-get-rcompletions))
- token-string)
- (when possible-completions
- (setq token-string (pop possible-completions))
- (or (completion-in-region (- (point) (length token-string))
- (point)
- possible-completions)
- 'none))))
-
- (defvar ess--cached-sp-objects nil)
-
- (defun ess--get-cached-completions (prefix &optional _point)
- (if (string-match-p "[]:$@[]" prefix)
- ;; call proc for objects
- (cdr (ess-r-get-rcompletions nil nil prefix))
- ;; else, get cached list of objects
- (with-ess-process-buffer 'no-error ;; use proc buf alist
- (ess-when-new-input last-cached-completions
- (if (and ess--cached-sp-objects
- (not (process-get *proc* 'sp-for-ac-changed?)))
- ;; if global cache is already there, only re-read local .GlobalEnv
- (progn
- (unless ess-sl-modtime-alist
- ;; initialize if empty
- (setq ess-sl-modtime-alist '((".GlobalEnv" nil))))
- ;; fixme: Make adaptive. Not on all remotes are slow; For lots of
- ;; objects in .GlobalEnv,locals could also be slow.
- (unless (file-remote-p default-directory)
- (ess-extract-onames-from-alist ess-sl-modtime-alist 1 'force)))
- (if ess--cached-sp-objects
- (ess-get-modtime-list 'ess--cached-sp-objects 'exclude-first)
- (ess-get-modtime-list)
- (setq ess--cached-sp-objects (cdr ess-sl-modtime-alist)))
- ;; reread new package, but not rda, much faster and not needed anyways
- (process-put *proc* 'sp-for-ac-changed? nil)))
- (apply 'append
- (cl-cddar ess-sl-modtime-alist) ; .GlobalEnv
- (mapcar 'cddr ess--cached-sp-objects)))))
-
- ;;; ARGUMENTS
-
- (defvar ess-r--funargs-pre-cache
- '(("plot"
- (("graphics")
- (("x" . "") ("y" . "NULL") ("type" . "p") ("xlim" . "NULL") ("ylim" . "NULL") ("log" . "") ("main" . "NULL") ("sub" . "NULL") ("xlab" . "NULL") ("ylab" . "NULL")
- ("ann" . "par(\"ann\")") ("axes" . "TRUE") ("frame.plot" . "axes") ("panel.first" . "NULL") ("panel.last" . "NULL") ("asp" . "NA") ("..." . ""))
- ("x" "y" "..." "ci" "type" "xlab" "ylab" "ylim" "main" "ci.col" "ci.type" "max.mfrow" "ask" "mar" "oma" "mgp" "xpd" "cex.main" "verbose" "scale" "xlim" "log" "sub" "ann" "axes" "frame.plot"
- "panel.first" "panel.last" "asp" "center" "edge.root" "nodePar" "edgePar" "leaflab" "dLeaf" "xaxt" "yaxt" "horiz"
- "zero.line" "verticals" "col.01line" "pch" "legend.text" "formula" "data" "subset" "to" "from" "newpage" "vp" "labels"
- "hang" "freq" "density" "angle" "col" "border" "lty" "add" "predicted.values" "intervals" "separator" "col.predicted"
- "col.intervals" "col.separator" "lty.predicted" "lty.intervals" "lty.separator" "plot.type" "main2" "par.fit" "grid"
- "panel" "cex" "dimen" "abbrev" "which" "caption" "sub.caption" "id.n" "labels.id" "cex.id" "qqline" "cook.levels"
- "add.smooth" "label.pos" "cex.caption" "rows" "levels" "conf" "absVal" "ci.lty" "xval" "do.points" "col.points" "cex.points"
- "col.hor" "col.vert" "lwd" "set.pars" "range.bars" "col.range" "xy.labels" "xy.lines" "nc" "yax.flip" "mar.multi" "oma.multi")))
- ("print"
- (("base")
- (("x" . "") ("digits" . "NULL") ("quote" . "TRUE") ("na.print" . "NULL") ("print.gap" . "NULL") ("right" . "FALSE") ("max" . "NULL") ("useSource" . "TRUE") ("..." . ""))
- ("x" "..." "digits" "signif.stars" "intercept" "tol" "se" "sort" "verbose" "indent" "style" ".bibstyle" "prefix" "vsep" "minlevel" "quote" "right" "row.names" "max" "na.print" "print.gap"
- "useSource" "diag" "upper" "justify" "title" "max.levels" "width" "steps" "showEnv" "newpage" "vp" "cutoff" "max.level" "give.attr" "units" "abbrCollate" "print.x" "deparse" "locale" "symbolic.cor"
- "loadings" "zero.print" "calendar"))))
- "Alist of cached arguments for time consuming functions.")
-
- ;;; HELP
-
- (defun ess-r-get-object-help-string (sym)
- "Help string for ac."
- (let ((proc (ess-get-next-available-process)))
- (if (null proc)
- "No free ESS process found"
- (let ((buf (get-buffer-create " *ess-command-output*")))
- (when (string-match ":+\\(.*\\)" sym)
- (setq sym (match-string 1 sym)))
- (with-current-buffer (process-buffer proc)
- (ess-with-current-buffer buf
- (ess--flush-help-into-current-buffer sym nil)))
- (with-current-buffer buf
- (ess-help-underline)
- (goto-char (point-min))
- (buffer-string))))))
-
- (defun ess-r-get-arg-help-string (sym &optional proc)
- "Help string for ac."
- (setq sym (replace-regexp-in-string " *= *\\'" "" sym))
- (let ((proc (or proc (ess-get-next-available-process))))
- (if (null proc)
- "No free ESS process found"
- (let ((fun (car ess--fn-name-start-cache)))
- (with-current-buffer (ess-command (format ".ess_arg_help('%s','%s')\n" sym fun)
- nil nil nil nil proc)
- (goto-char (point-min))
- (forward-line)
- (buffer-substring-no-properties (point) (point-max)))))))
-
- ;;; COMPANY
- ;;; https://company-mode.github.io/
-
- (defun company-R-objects (command &optional arg &rest ignored)
- (interactive (list 'interactive))
- (cl-case command
- (interactive (company-begin-backend 'company-R-objects))
- (prefix (unless (ess-inside-string-or-comment-p)
- (let ((start (ess-symbol-start)))
- (when start
- (buffer-substring-no-properties start (point))))))
- (candidates (let ((proc (ess-get-next-available-process)))
- (when proc
- (with-current-buffer (process-buffer proc)
- (all-completions arg (ess--get-cached-completions arg))))))
- (doc-buffer (company-doc-buffer (ess-r-get-object-help-string arg)))))
-
- (defun company-R-args (command &optional arg &rest ignored)
- (interactive (list 'interactive))
- (cl-case command
- (interactive (company-begin-backend 'company-R-args))
- (prefix (unless (ess-inside-string-or-comment-p)
- (let ((start (ess-arg-start)))
- (when start
- (let ((prefix (buffer-substring-no-properties start (point))))
- (if ess-company-arg-prefix-length
- (cons prefix (>= (length prefix)
- ess-company-arg-prefix-length))
- prefix))))))
- (candidates (let* ((proc (ess-get-next-available-process))
- (args (delete "..." (nth 2 (ess-function-arguments
- (car ess--fn-name-start-cache) proc))))
- (args (mapcar (lambda (a) (concat a ess-R-argument-suffix))
- args)))
- (all-completions arg args)))
- (meta (let ((proc (ess-get-next-available-process)))
- (when (and proc
- (with-current-buffer (process-buffer proc)
- (not (file-remote-p default-directory))))
- ;; fixme: ideally meta should be fetched with args
- (let ((doc (ess-r-get-arg-help-string arg proc)))
- (replace-regexp-in-string "^ +\\| +$" ""
- (replace-regexp-in-string "[ \t\n]+" " " doc))))))
- (sorted t)
- (require-match 'never)
- (doc-buffer (company-doc-buffer (ess-r-get-arg-help-string arg)))))
-
- (defun company-R-library (command &optional arg &rest ignored)
- (interactive (list 'interactive))
- (cl-case command
- (interactive (company-begin-backend 'company-R-library))
- (prefix (and (member (car (ess--fn-name-start))
- '("library" "require"))
- (let ((start (ess-symbol-start)))
- (and start (buffer-substring start (point))))))
- (candidates (all-completions arg (ess-installed-packages)))
- (annotation "<pkg>")
- (duplicates nil)
- (sorted t)))
-
- ;; FIXME: There's a lot of overlap between `ess-r-package-completion'
- ;; and `company-R-library'. Can we merge them somehow?
- (defun ess-r-package-completion ()
- "Return installed packages if in a call to library or require.
- Return format suitable for `completion-at-point-functions'."
- (when (member (car (ess--fn-name-start))
- '("library" "require"))
- (list (ess-symbol-start)
- (point)
- (ess-installed-packages)
- :annotation-function
- (lambda (_) " <pkg>"))))
-
- ;;; AC SOURCES
- ;;; http://cx4a.org/software/auto-complete/index.html
- ;; auto-complete is de-facto unmaintained, users should switch to `company-mode'.
-
- (defvar ac-source-R
- '((prefix . ess-ac-start)
- ;; (requires . 0) ::)
- (candidates . ess-ac-candidates)
- ;; (action . ess-ac-action-args) ;; interfere with ac-fallback mechanism on RET (which is extremely annoying in inferior buffers)
- (document . ess-ac-help))
- "Combined ad-completion source for R function arguments and R objects.")
- (make-obsolete-variable 'ac-source-R "Use company-mode instead" "ESS 19.04")
-
- (defun ess-ac-start ()
- (when (ess-process-live-p)
- (or (ess-arg-start)
- (ess-symbol-start))))
- (make-obsolete-variable 'ess-ac-start "Use company-mode instead" "ESS 19.04")
-
- (defun ess-ac-candidates ()
- "OBJECTS + ARGS."
- (let ((args (with-no-warnings
- ;; suppress obsolete warnings
- (ess-ac-args))))
- ;; sort of intrusive but right
- (if (and ac-auto-start
- (< (length ac-prefix) ac-auto-start))
- args
- (if args
- (append args (with-no-warnings
- ;; suppress obsolete warnings
- (ess-ac-objects t)))
- (with-no-warnings
- ;; suppress obsolete warnings
- (ess-ac-objects))))))
- (make-obsolete-variable 'ess-ac-candidates "Use company-mode instead" "ESS 19.04")
-
- (defun ess-ac-help (sym)
- (if (string-match-p "= *\\'" sym)
- (ess-r-get-arg-help-string sym)
- (ess-r-get-object-help-string sym)))
- (make-obsolete-variable 'ess-ac-help "Use company-mode instead" "ESS 19.04")
-
- ;; OBJECTS
- (defvar ac-source-R-objects
- '((prefix . ess-symbol-start)
- ;; (requires . 2)
- (candidates . ess-ac-objects)
- (document . ess-r-get-object-help-string))
- "Auto-completion source for R objects.")
- (make-obsolete-variable 'ac-source-R-objects "Use company-mode instead" "ESS 19.04")
-
- (defun ess-ac-objects (&optional no-kill)
- "Get all cached objects."
- (declare (obsolete "Use company-mode instead" "ESS 19.04"))
- (let ((aprf ac-prefix))
- (when (and aprf (ess-process-live-p))
- (unless no-kill ;; workaround
- (kill-local-variable 'ac-use-comphist))
- (ess--get-cached-completions aprf ac-point))))
-
- ;; ARGS
- (defvar ac-source-R-args
- '((prefix . ess-arg-start)
- ;; (requires . 0)
- (candidates . ess-ac-args)
- ;; (action . ess-ac-action-args)
- (document . ess-r-get-arg-help-string))
- "Auto-completion source for R function arguments.")
- (make-obsolete-variable 'ac-source-R-args "Use company-mode instead" "ESS 19.04")
-
- (defun ess-ac-args ()
- "Get the args of the function when inside parentheses."
- (declare (obsolete "Use company-mode-instead" "ESS 19.04"))
- (when (and ess--fn-name-start-cache ;; set in a call to ess-arg-start
- (ess-process-live-p))
- (let ((args (nth 2 (ess-function-arguments (car ess--fn-name-start-cache)))))
- (if args
- (setq-local ac-use-comphist nil)
- (kill-local-variable 'ac-use-comphist))
- (delete "..." args)
- (mapcar (lambda (a) (concat a ess-R-argument-suffix))
- args))))
-
- (provide 'ess-r-completion)
-
- ;;; ess-r-completion.el ends here
|