|
;;; ess-utils.el --- General Emacs utility functions used by ESS -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 1998--2010 A.J. Rossini, Richard M. Heiberger, Martin
|
|
;; Maechler, Kurt Hornik, Rodney Sparapani, and Stephen Eglen.
|
|
;; Copyright (C) 2011--2017 A.J. Rossini, Richard M. Heiberger, Martin Maechler,
|
|
;; Kurt Hornik, Rodney Sparapani, Stephen Eglen and Vitalie Spinu.
|
|
|
|
;; Author: Martin Maechler <maechler@stat.math.ethz.ch>
|
|
;; Created: 9 Sept 1998
|
|
;; Maintainer: ESS-core <ESS-core@r-project.org>
|
|
|
|
;; This file is part of ESS (Emacs Speaks Statistics).
|
|
|
|
;; 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:
|
|
;; Various utilities for ESS.
|
|
|
|
;;; Code:
|
|
(require 'cl-lib)
|
|
(require 'comint)
|
|
(eval-when-compile
|
|
(require 'tramp))
|
|
;; The only ESS file this file should depend on is ess-custom.el
|
|
(require 'ess-custom)
|
|
|
|
(defvar ac-modes)
|
|
(declare-function ess-eval-linewise "ess-inf" (text &optional invisibly eob even-empty wait-last-prompt sleep-sec wait-sec))
|
|
(declare-function color-lighten-name "color" (name percent))
|
|
(declare-function tramp-dissect-file-name "tramp" (name &optional nodefault))
|
|
;; The following declares can be removed once we drop Emacs 25
|
|
(declare-function tramp-file-name-method "tramp")
|
|
(declare-function tramp-file-name-user "tramp")
|
|
(declare-function tramp-file-name-host "tramp")
|
|
(declare-function tramp-file-name-localname "tramp")
|
|
(declare-function tramp-file-name-hop "tramp")
|
|
|
|
|
|
;;*;; elisp tools
|
|
|
|
(defun ess-next-code-line (&optional arg skip-to-eob)
|
|
"Move ARG lines of code forward (backward if ARG is negative).
|
|
If `ess-eval-empty' is non-nil, skip past all empty and comment
|
|
lines. ARG is 1 by default. If ARG is 0 only comments are skipped
|
|
forward. Don't skip the last empty and comment lines in the
|
|
buffer unless SKIP-TO-EOB is non-nil. On success, return 0.
|
|
Otherwise go as far as possible and return -1."
|
|
(interactive "p")
|
|
(if (or ess-eval-empty
|
|
(and (fboundp 'ess-roxy-entry-p)
|
|
(ess-roxy-entry-p)))
|
|
(forward-line arg)
|
|
(setq arg (or arg 1))
|
|
(beginning-of-line)
|
|
(let ((pos (point))
|
|
(inc (if (>= arg 0) 1 -1))
|
|
(cnt (if (= arg 0) 1 arg))
|
|
(out 0))
|
|
;; when orig-arg == 0, we skip only comments
|
|
(while (and (/= cnt 0) (= out 0))
|
|
(unless (= arg 0)
|
|
(setq out (forward-line inc))) ; out==0 means success
|
|
(comment-beginning)
|
|
(beginning-of-line)
|
|
(forward-comment (* inc (buffer-size))) ;; as suggested in info file
|
|
(if (or skip-to-eob
|
|
(not (looking-at ess-no-skip-regexp))) ;; don't go to eob or whatever
|
|
(setq cnt (- cnt inc))
|
|
(goto-char pos)
|
|
(setq cnt 0)
|
|
(forward-line inc)) ;; stop at next empty line
|
|
(setq pos (point)))
|
|
(goto-char pos)
|
|
out)))
|
|
|
|
(defun ess-goto-line (line)
|
|
"Go to LINE in the widened buffer."
|
|
(save-restriction
|
|
(widen)
|
|
(goto-char (point-min))
|
|
(forward-line (1- line))))
|
|
|
|
(defun ess-skip-thing (thing)
|
|
"Leave point at the end of THING.
|
|
THING can be 'function, 'paragraph, or 'line."
|
|
(cond
|
|
((eql thing 'line) (goto-char (line-end-position)))
|
|
((eql thing 'paragraph) (forward-paragraph))
|
|
((eql thing 'function) (end-of-defun) (skip-chars-backward " \t\n"))))
|
|
|
|
(defun ess-search-except (regexp &optional except backward)
|
|
"Search for a REGEXP and store as match 1.
|
|
Optionally ignore strings that match EXCEPT. If BACKWARD is
|
|
non-nil, search backward."
|
|
(let ((continue t) (exit nil))
|
|
(while continue
|
|
(if (or (and backward (search-backward-regexp regexp nil t))
|
|
(and (not backward) (search-forward-regexp regexp nil t)))
|
|
(progn
|
|
(setq exit (match-string 1))
|
|
(setq continue (and except (string-match except exit)))
|
|
(if continue (setq exit nil)))
|
|
;;else
|
|
(setq continue nil)))
|
|
exit))
|
|
|
|
(defun ess-save-and-set-local-variables ()
|
|
"If buffer was modified, save file and set Local Variables if defined.
|
|
Return t if buffer was modified, nil otherwise."
|
|
(let ((ess-temp-return-value (buffer-modified-p)))
|
|
(save-buffer)
|
|
(hack-local-variables)
|
|
ess-temp-return-value))
|
|
|
|
(defun ess-get-file-or-buffer (file-or-buffer)
|
|
"Return FILE-OR-BUFFER if it is a buffer.
|
|
Otherwise return the buffer associated with the file which must
|
|
be qualified by it's path; if the buffer does not exist, return
|
|
nil."
|
|
(declare (side-effect-free t)
|
|
(obsolete nil "ESS 19.04"))
|
|
(if (bufferp file-or-buffer)
|
|
file-or-buffer
|
|
(find-buffer-visiting file-or-buffer)))
|
|
|
|
(defun ess-file-content (file)
|
|
"Return the content of FILE as string."
|
|
(if (file-exists-p file)
|
|
(with-temp-buffer
|
|
(insert-file-contents-literally file)
|
|
(buffer-string))
|
|
(error "File '%s' does not exist" file)))
|
|
|
|
(defun ess-uniq (list predicate)
|
|
"Uniquify LIST, stably, deleting elements using PREDICATE.
|
|
Return the list with subsequent duplicate items removed by side effects.
|
|
PREDICATE is called with an element of LIST and a list of elements from LIST,
|
|
and should return the list of elements with occurrences of the element removed.
|
|
This function will work even if LIST is unsorted. See also `delete-dups'."
|
|
(declare (obsolete 'delete-dups "ESS 19.04"))
|
|
(let ((list list))
|
|
(while list
|
|
(setq list (setcdr list (funcall predicate (car list) (cdr list))))))
|
|
list)
|
|
|
|
(define-obsolete-function-alias 'ess-uniq-list 'delete-dups "ESS 19.04")
|
|
|
|
(defalias 'ess-flatten-list
|
|
;; `flatten-tree' is a function in Emacs 27
|
|
(if (fboundp 'flatten-tree)
|
|
'flatten-tree
|
|
(lambda (list)
|
|
"Take the arguments and flatten them into one long LIST.
|
|
Drops 'nil' entries."
|
|
;; Taken from lpr.el
|
|
;; `lpr-flatten-list' is defined here (copied from "message.el" and
|
|
;; enhanced to handle dotted pairs as well) until we can get some
|
|
;; sensible autoloads, or `flatten-list' gets put somewhere decent.
|
|
(ess-flatten-list-1 list))))
|
|
|
|
(defun ess-flatten-list-1 (list)
|
|
"Internal helper for `ess-flatten-list', which see for LIST."
|
|
(cond
|
|
((null list) (list))
|
|
((consp list)
|
|
(append (ess-flatten-list-1 (car list))
|
|
(ess-flatten-list-1 (cdr list))))
|
|
(t (list list))))
|
|
|
|
(define-obsolete-function-alias 'ess-delete-blank-lines
|
|
'delete-blank-lines "ESS 19.04")
|
|
|
|
(define-obsolete-function-alias 'ess-line-to-list-of-words #'split-string "ESS 19.04")
|
|
|
|
|
|
;;*;; System
|
|
|
|
(defun ess-revert-wisely ()
|
|
"Revert from disk if file and buffer last modification times are different."
|
|
(interactive)
|
|
;; whether or not a revert is needed, force load local variables
|
|
;; for example, suppose that you change the local variables and then
|
|
;; save the file, a revert is unneeded, but a force load is
|
|
(hack-local-variables)
|
|
(unless (verify-visited-file-modtime (current-buffer))
|
|
(progn
|
|
(let ((ess-temp-store-point (point)))
|
|
(revert-buffer t t)
|
|
(goto-char ess-temp-store-point))
|
|
t)))
|
|
|
|
(define-obsolete-function-alias 'ess-find-exec 'ess-find-exec-completions "ESS 19.04")
|
|
(defun ess-find-exec-completions (root)
|
|
"Given the ROOT of an executable file name, find all possible completions.
|
|
Search for the executables in the variable `exec-path'."
|
|
(let (executables)
|
|
(dolist (dir exec-path)
|
|
(when (and (> (length dir) 0)
|
|
(file-accessible-directory-p dir))
|
|
;; the first test above excludes "" from exec-path, which can be
|
|
;; problematic with Tramp.
|
|
(dolist (f (file-name-all-completions root dir))
|
|
(setq f (expand-file-name f dir))
|
|
(when (and (file-executable-p f)
|
|
(not (backup-file-name-p f))
|
|
(not (file-directory-p f)))
|
|
(push f executables)))))
|
|
executables))
|
|
|
|
(defun ess-drop-non-directories (file-strings)
|
|
"Drop all entries in FILE-STRINGS that do not \"look like\" directories."
|
|
(ess-flatten-list (mapcar 'file-name-directory file-strings)))
|
|
|
|
(defun ess--parent-dir (path n)
|
|
"Return Nth parent of PATH."
|
|
(let ((opath path))
|
|
(while (and path (> n 0))
|
|
(setq path (file-name-directory (directory-file-name opath)))
|
|
(if (equal path opath)
|
|
(setq path nil)
|
|
(setq opath path
|
|
n (1- n))))
|
|
path))
|
|
|
|
|
|
;;*;; Interaction with inferiors
|
|
|
|
(defmacro ess-when-new-input (time-var &rest body)
|
|
"BODY is evaluate only if the value of process variable TIME-VAR
|
|
is bigger than the time of the last user input (stored in
|
|
`last-eval' process variable). TIME-VAR is the name of the
|
|
process variable which holds the access time. See the code for
|
|
`ess-synchronize-dirs' and `ess-cache-search-list'.
|
|
|
|
Returns nil when no current process, or process is busy, or
|
|
time-var > last-eval. Otherwise, execute BODY and return the last
|
|
value.
|
|
|
|
If BODY is executed, set process variable TIME-VAR
|
|
to (current-time).
|
|
|
|
Variable *proc* is bound to the current process during the
|
|
evaluation of BODY.
|
|
|
|
Should be used in `ess-idle-timer-functions' which call the
|
|
process to avoid excessive requests."
|
|
(declare (indent 1) (debug t))
|
|
`(with-ess-process-buffer 'no-error
|
|
(let ((le (process-get *proc* 'last-eval))
|
|
(tv (process-get *proc* ',time-var)))
|
|
(when (and (or (null tv) (null le) (time-less-p tv le))
|
|
(not (process-get *proc* 'busy)))
|
|
(let ((out (progn ,@body)))
|
|
(process-put *proc* ',time-var (current-time))
|
|
out)))))
|
|
|
|
|
|
;;; Emacs Integration
|
|
|
|
(defun ess-derived-mode-p ()
|
|
"Non-nil if the current major mode is an ESS major mode."
|
|
(or (derived-mode-p 'ess-mode)
|
|
(derived-mode-p 'ess-julia-mode)))
|
|
|
|
(defun ess--generate-eval-visibly-submenu (_menu)
|
|
'(["yes" (lambda () (interactive) (setq ess-eval-visibly t))
|
|
:style radio :enable t :selected (eq ess-eval-visibly t)]
|
|
["nowait" (lambda () (interactive) (setq ess-eval-visibly 'nowait))
|
|
:style radio :enable t :selected (eq ess-eval-visibly 'nowait) ]
|
|
["no" (lambda () (interactive) (setq ess-eval-visibly nil))
|
|
:style radio :enable t :selected (eq ess-eval-visibly nil) ]))
|
|
|
|
;; Font Lock
|
|
|
|
(defun ess--fl-keywords-values ()
|
|
"Return a cons (STANDARD-VALUE . CUSTOM-VALUE) of `ess-font-lock-keywords'."
|
|
(let ((sym ess-font-lock-keywords))
|
|
(if (and (symbolp sym)
|
|
(custom-variable-p sym))
|
|
(cons
|
|
(eval (car (get sym 'standard-value)))
|
|
(symbol-value sym))
|
|
(error "`ess-font-lock-keywords' must be a symbol of a custom variable"))))
|
|
|
|
(defun ess--extract-fl-keywords ()
|
|
(let ((values (ess--fl-keywords-values)))
|
|
(mapcar (lambda (kv)
|
|
(let ((cust-kv (assoc (car kv) (cdr values))))
|
|
(when cust-kv
|
|
(setcdr kv (cdr cust-kv))))
|
|
kv)
|
|
(copy-alist (car values)))))
|
|
|
|
(defun ess-build-font-lock-keywords ()
|
|
"Retrieve `font-lock-keywords' from ess-[dialect]-font-lock-keywords.
|
|
Merge the customized values of that variable on top of the
|
|
standard values and return the new list. For this to work,
|
|
`ess-font-lock-keywords' should be a name of the
|
|
ess-[dialect]-font-lock-keywords variable."
|
|
(delq nil
|
|
(mapcar (lambda (c)
|
|
(when (cdr c)
|
|
(symbol-value (car c))))
|
|
(ess--extract-fl-keywords))))
|
|
|
|
(defun ess-font-lock-toggle-keyword (keyword)
|
|
"Toggle KEYWORD font-lock."
|
|
(interactive
|
|
(list (intern (ess-completing-read
|
|
"Keyword to toggle"
|
|
(mapcar (lambda (el) (symbol-name (car el)))
|
|
(car (ess--fl-keywords-values)))
|
|
nil t))))
|
|
(let* ((values (ess--fl-keywords-values))
|
|
(kwd (cond
|
|
;; already in custom values
|
|
((assoc keyword (cdr values)))
|
|
;; if keyword is not already in custom values (can happen if
|
|
;; we add new keywords but the user has the old value saved in
|
|
;; .emacs-custom.el)
|
|
((let ((kwd (assoc keyword (car values)))
|
|
(sym ess-font-lock-keywords))
|
|
(when kwd
|
|
(set sym (cons kwd (symbol-value sym)))
|
|
kwd)))
|
|
(t (error "Invalid keyword %s" keyword)))))
|
|
(setcdr kwd (not (cdr kwd)))
|
|
(let ((mode major-mode)
|
|
(dialect ess-dialect))
|
|
;; refresh font-lock defaults in all relevant buffers
|
|
(mapc (lambda (b)
|
|
(with-current-buffer b
|
|
(when (and (eq major-mode mode)
|
|
(eq ess-dialect dialect))
|
|
(font-lock-refresh-defaults))))
|
|
(buffer-list)))))
|
|
|
|
(defun ess--generate-font-lock-submenu (_menu)
|
|
"Generate ESS font-lock submenu."
|
|
(append (mapcar (lambda (el)
|
|
`[,(symbol-name (car el))
|
|
(lambda () (interactive)
|
|
(ess-font-lock-toggle-keyword ',(car el)))
|
|
:style toggle
|
|
:enable t
|
|
:selected ,(cdr el)])
|
|
(ess--extract-fl-keywords))
|
|
(list "-----"
|
|
["Save to custom"
|
|
(lambda () (interactive)
|
|
(customize-save-variable ess-font-lock-keywords
|
|
(ess--extract-fl-keywords)))
|
|
t])))
|
|
|
|
|
|
;;; External modes
|
|
|
|
;; Define these here for the byte compiler since ido dynamically
|
|
;; let-binds them:
|
|
(defvar ido-choice-list)
|
|
(defvar ido-context-switch-command)
|
|
(defvar ido-directory-too-big)
|
|
(defvar ido-directory-nonreadable)
|
|
(defvar ido-current-directory)
|
|
(defvar ido-enable-flex-matching)
|
|
(declare-function ido-read-internal "ido" (item prompt hist &optional default require-match initial))
|
|
|
|
(defun ess-completing-read (prompt collection &optional predicate
|
|
require-match initial-input hist def)
|
|
"Read a string in the minibuffer, with completion.
|
|
Use `ido-completing-read' if IDO interface is present, or fall
|
|
back on classical `completing-read' otherwise. Meaning of PROMPT,
|
|
COLLECTION, PREDICATE, REQUIRE-MATCH, INITIAL-INPUT, HIST, and
|
|
DEF is as in `completing-read'. PROMPT is automatically suffixed
|
|
with ': ' and (default %s) when needed. If HIST is nil use
|
|
`ess--completing-hist' as history. See also `ess-use-ido'."
|
|
(let ((use-ido (and ess-use-ido (require 'ido nil t))))
|
|
(setq hist (or hist 'ess--completing-hist))
|
|
(when (and def (not use-ido)) ;; ido places in front and highlights the default
|
|
(setq prompt (format "%s(default %s)" prompt def)))
|
|
(setq prompt (concat prompt ": "))
|
|
(if use-ido
|
|
(let ((reset-ido (and use-ido (not ido-mode))) ;people not using ido but having it)
|
|
(ido-current-directory nil)
|
|
(ido-directory-nonreadable nil)
|
|
(ido-directory-too-big nil)
|
|
(ido-context-switch-command 'ignore)
|
|
(ido-enable-flex-matching ess-ido-flex-matching) ;it's fast and useful, may be get into options
|
|
(ido-choice-list (copy-sequence collection)) ;ido removes the match (reported)
|
|
sel)
|
|
(unwind-protect
|
|
(progn
|
|
(add-hook 'minibuffer-setup-hook 'ido-minibuffer-setup)
|
|
(add-hook 'choose-completion-string-functions 'ido-choose-completion-string)
|
|
(setq sel (ido-read-internal 'list prompt hist def require-match initial-input))
|
|
(when hist ;; ido does not push into hist the whole match if C-SPC or RET is used (reported)
|
|
(unless (string= sel (car (symbol-value hist)))
|
|
(set hist (cons sel (symbol-value hist))))))
|
|
(when reset-ido
|
|
(remove-hook 'minibuffer-setup-hook 'ido-minibuffer-setup)
|
|
(remove-hook 'choose-completion-string-functions 'ido-choose-completion-string)))
|
|
sel)
|
|
;; else usual completion
|
|
(completing-read prompt collection predicate require-match initial-input hist def))))
|
|
|
|
(defun ess--setup-auto-complete (sources &optional inferior)
|
|
"Setup auto-complete depending on user settings.
|
|
SOURCES gets added to `ac-sources', INFERIOR should be t for
|
|
inferior buffers."
|
|
;; auto-complete
|
|
(when (and (boundp 'ac-sources)
|
|
(if inferior
|
|
(eq ess-use-auto-complete t)
|
|
ess-use-auto-complete))
|
|
(add-to-list 'ac-modes major-mode)
|
|
;; files should be in front; ugly, but needed
|
|
(setq ac-sources
|
|
(delq 'ac-source-filename ac-sources))
|
|
(mapc (lambda (el) (add-to-list 'ac-sources el))
|
|
sources)
|
|
(add-to-list 'ac-sources 'ac-source-filename)))
|
|
|
|
(defun ess--setup-company (sources &optional inferior)
|
|
"Setup company depending on user settings.
|
|
SOURCES gets added to `company-backends', and when t, INFERIOR
|
|
specifies inferior buffers."
|
|
;; company
|
|
(when (and (boundp 'company-backends)
|
|
(if inferior
|
|
(eq ess-use-company t)
|
|
ess-use-company))
|
|
(setq-local company-backends
|
|
(cl-copy-list (append sources company-backends)))
|
|
(delq 'company-capf company-backends)))
|
|
|
|
(defmacro ess--execute-electric-command (map &optional prompt wait exit-form &rest args)
|
|
"Execute single-key commands defined in MAP till a key is pressed which is not part of map.
|
|
Single-key input commands are those that once executed do not
|
|
require the prefix command for subsequent invocation. Return the
|
|
value of the lastly executed command. PROMPT is passed to
|
|
`read-event'.
|
|
|
|
If WAIT is t, wait for next input and ignore the keystroke which
|
|
triggered the command.
|
|
|
|
Each command in map should accept one at least one argument, the
|
|
most recent event (as read by `read-event'). ARGS are the
|
|
supplementary arguments passed to the commands.
|
|
|
|
EXIT-FORM should be supplied for a more refined control of the
|
|
read-even loop. The loop is exited when EXIT-FORM evaluates to
|
|
t. See examples in the tracebug code."
|
|
;;VS[09-06-2013]: check: it seems that set-temporary-overlay-map is designed
|
|
;;for this type of things; see also repeat.el package.
|
|
`(let* ((ev last-command-event)
|
|
(command (lookup-key ,map (vector ev)))
|
|
out exit )
|
|
(if (not (or ,wait command))
|
|
(message "%s is undefined" (key-description (this-command-keys)))
|
|
(unless ,wait
|
|
(setq out (and command (funcall command ev ,@args))))
|
|
(while (and (not exit)
|
|
(setq command
|
|
(lookup-key ,map
|
|
(vector (setq ev (read-event ,prompt))))))
|
|
(setq out (funcall command ev ,@args))
|
|
(sleep-for .01)
|
|
(setq exit ,exit-form))
|
|
(unless exit ;; push only if an event triggered the exit
|
|
(push ev unread-command-events))
|
|
out)))
|
|
|
|
(cl-defgeneric ess-build-tags-command ()
|
|
"Command passed to generate tags.
|
|
If nil, `ess-build-tags-for-directory' uses the mode's imenu
|
|
expression. Otherwise, it should be a string with two %s
|
|
formats: one for directory and another for the output file."
|
|
nil)
|
|
|
|
|
|
;;; Emacs itself
|
|
|
|
(defun ess-yank-cleaned-commands ()
|
|
"Yank and strip the code, leaving only (R/S/Lsp/..) commands.
|
|
Deletes any lines not beginning with a prompt, and then removes
|
|
the prompt from those lines that remain. Invoke this command with
|
|
\\[universal-argument] \\[universal-argument] \\<ess-mode-map>\\[yank]."
|
|
(setq yank-window-start (window-start))
|
|
(let ((beg (point)))
|
|
(push-mark beg)
|
|
(setq this-command t)
|
|
(insert-for-yank (current-kill 0))
|
|
(when (and (require 'ess-trns) (fboundp 'ess-transcript-clean-region))
|
|
(ess-transcript-clean-region beg (point) nil))
|
|
(if (eq (point) beg)
|
|
(message "No commands found"))
|
|
(if (eq this-command t)
|
|
(setq this-command 'yank))))
|
|
|
|
(defun ess-yank (&optional arg)
|
|
"Call `ess-yank-cleaned-commands' if ARG is 16.
|
|
With double prefix ARG (\\[universal-argument]
|
|
\\[universal-argument]) call `ess-yank-cleaned-commands'."
|
|
(interactive "*P")
|
|
(if (equal '(16) arg)
|
|
(ess-yank-cleaned-commands)
|
|
(let* ((remapped (command-remapping 'yank (point)))
|
|
(command (cond ((eq remapped 'ess-yank) 'yank)
|
|
((null remapped) 'yank)
|
|
(t remapped))))
|
|
(funcall command arg))))
|
|
|
|
(put 'ess-yank 'delete-selection 'yank)
|
|
|
|
(defun ess-build-tags-for-directory (dir tagfile)
|
|
"Ask for directory and tag file and build tags for current dialect.
|
|
If the current language defines `ess-build-tags-command' use it
|
|
and ask the subprocess to build the tags. Otherwise use imenu
|
|
regexp and call find .. | etags .. in a shell command. You must
|
|
have 'find' and 'etags' programs installed.
|
|
|
|
Use M-. to navigate to a tag. \\[visit-tags-table] to
|
|
append/replace the currently used tag table.
|
|
|
|
If prefix is given, force tag generation based on imenu. Might be
|
|
useful when different language files are also present in the
|
|
directory (.cpp, .c etc)."
|
|
(interactive "DDirectory to tag:
|
|
GTags file (default TAGS): ")
|
|
(when (or (eq (length (file-name-nondirectory tagfile)) 0)
|
|
(file-directory-p tagfile))
|
|
(setq tagfile (concat (file-name-as-directory tagfile) "TAGS")))
|
|
;; Emacs find-tags doesn't play well with remote TAG files :(
|
|
(when (file-remote-p tagfile)
|
|
(require 'tramp)
|
|
(setq tagfile (with-parsed-tramp-file-name tagfile foo foo-localname)))
|
|
(when (file-remote-p dir)
|
|
(require 'tramp)
|
|
(setq dir (with-parsed-tramp-file-name dir foo foo-localname)))
|
|
(if (and (ess-build-tags-command) (null current-prefix-arg))
|
|
(ess-eval-linewise (format (ess-build-tags-command) dir tagfile))
|
|
;; else generate from imenu
|
|
(unless imenu-generic-expression
|
|
(error "No ess-tag-command found, and no imenu-generic-expression defined"))
|
|
(let* ((find-cmd
|
|
(format "find %s -type f -size 1M \\( -regex \".*\\.\\(cpp\\|jl\\|[RsrSch]\\(nw\\)?\\)$\" \\)" dir))
|
|
(regs (delq nil (mapcar (lambda (l)
|
|
(if (string-match "'" (cadr l))
|
|
nil ;; remove for time being
|
|
(format "/%s/\\%d/"
|
|
(replace-regexp-in-string "/" "\\/" (nth 1 l) t)
|
|
(nth 2 l))))
|
|
imenu-generic-expression)))
|
|
(tags-cmd (format "etags -o %s --regex='%s' -" tagfile
|
|
(mapconcat 'identity regs "' --regex='"))))
|
|
(message "Building tags: %s" tagfile)
|
|
(when (= 0 (shell-command (format "%s | %s" find-cmd tags-cmd)))
|
|
(message "Building tags .. ok!")))))
|
|
|
|
|
|
;;; UI
|
|
|
|
(defvar ess-current-region-overlay
|
|
(let ((overlay (make-overlay (point) (point))))
|
|
(overlay-put overlay 'face 'highlight)
|
|
overlay)
|
|
"The overlay for highlighting currently evaluated region or line.")
|
|
|
|
(defun ess-blink-region (start end)
|
|
"Blink from START to END depending on option `ess-blink-region'."
|
|
(when ess-blink-region
|
|
(move-overlay ess-current-region-overlay start end)
|
|
(run-with-timer ess-blink-delay nil
|
|
(lambda ()
|
|
(delete-overlay ess-current-region-overlay)))))
|
|
|
|
(defun ess-deactivate-mark ()
|
|
"Deactivate the mark, if active.
|
|
If `evil-mode' is on, switch to `evil-normal-state'."
|
|
(if (and (bound-and-true-p evil-mode)
|
|
(fboundp 'evil-visual-state-p)
|
|
(evil-visual-state-p))
|
|
(when (fboundp 'evil-normal-state)
|
|
(evil-normal-state))
|
|
(when mark-active
|
|
(deactivate-mark))))
|
|
|
|
;; SJE: 2009-01-30 -- this contribution from
|
|
;; Erik Iverson <iverson@biostat.wisc.edu>
|
|
|
|
(defun ess-tooltip-show-at-point (text xo yo)
|
|
"Show a tooltip displaying TEXT at (around) point.
|
|
XO and YO are x- and y-offsets for the toolbar from point."
|
|
(let (
|
|
(fx (frame-parameter nil 'left))
|
|
(fy (frame-parameter nil 'top))
|
|
(fw (frame-pixel-width))
|
|
(fh (frame-pixel-height))
|
|
frame-left frame-top my-x-offset my-y-offset)
|
|
;; The following comment was found before code looking much like that
|
|
;; of frame-left and frame-top below in the file
|
|
;; tooltip-help.el. I include it here for acknowledgment, and I did observe
|
|
;; the same behavior with the Emacs window maximized under Windows XP.
|
|
|
|
;; -----original comment--------
|
|
;; handles the case where (frame-parameter nil 'top) or
|
|
;; (frame-parameter nil 'left) return something like (+ -4).
|
|
;; This was the case where e.g. Emacs window is maximized, at
|
|
;; least on Windows XP. The handling code is "shamelessly
|
|
;; stolen" from cedet/speedbar/dframe.el
|
|
;; (contributed by Andrey Grigoriev)
|
|
(setq frame-left (if (not (consp fx))
|
|
fx
|
|
(if (eq (car fx) '-)
|
|
(- (display-pixel-width) (car (cdr fx)) fw)
|
|
(car (cdr fx)))))
|
|
(setq frame-top (if (not (consp fy))
|
|
fy
|
|
(if (eq (car fy) '-)
|
|
(- (display-pixel-height) (car (cdr fy)) fh)
|
|
(car (cdr fy)))))
|
|
;; calculate the offset from point, use xo and yo to adjust to preference
|
|
(setq my-x-offset (+ (car(window-inside-pixel-edges))
|
|
(car(posn-x-y (posn-at-point)))
|
|
frame-left xo))
|
|
(setq my-y-offset (+ (cadr(window-inside-pixel-edges))
|
|
(cdr(posn-x-y (posn-at-point)))
|
|
frame-top yo))
|
|
(let ((tooltip-frame-parameters
|
|
(cons (cons 'top my-y-offset)
|
|
(cons (cons 'left my-x-offset)
|
|
tooltip-frame-parameters))))
|
|
(tooltip-show text))))
|
|
|
|
(defun ess-select-frame-set-input-focus (frame)
|
|
"Select FRAME, raise it, and set input focus, if possible.
|
|
Copied almost verbatim from gnus-utils.el (but with test for mac added)."
|
|
;; The function `select-frame-set-input-focus' won't set
|
|
;; the input focus under Emacs 21.2 and X window system.
|
|
;;((fboundp 'select-frame-set-input-focus)
|
|
;; (defalias 'gnus-select-frame-set-input-focus
|
|
;; 'select-frame-set-input-focus)
|
|
;; (select-frame-set-input-focus frame))
|
|
(raise-frame frame)
|
|
(select-frame frame)
|
|
(cond ((and
|
|
(memq window-system '(x mac))
|
|
(fboundp 'x-focus-frame))
|
|
(x-focus-frame frame))
|
|
((and (eq window-system 'w32)
|
|
;; silence byte compiler warnings about w32-fns
|
|
(fboundp 'w32-focus-frame))
|
|
(w32-focus-frame frame)))
|
|
(when focus-follows-mouse
|
|
(set-mouse-position frame (1- (frame-width frame)) 0)))
|
|
|
|
(define-obsolete-function-alias 'ess-do-auto-fill 'do-auto-fill "ESS 19.04")
|
|
|
|
|
|
;;; Syntax
|
|
|
|
(defun ess-containing-sexp-position ()
|
|
"Return the `cadr' of `syntax-ppss'."
|
|
(cadr (syntax-ppss)))
|
|
|
|
(defun ess-code-end-position ()
|
|
"Like (line-end-position) but stops at comments."
|
|
(save-excursion
|
|
(goto-char (1+ (line-end-position)))
|
|
(forward-comment -1)
|
|
(point)))
|
|
|
|
;; FIXME: The following function pattern stuff is specific to R but is
|
|
;; used throughout ESS
|
|
(defvar ess-r-set-function-start
|
|
;; [MGAR].. <=> {setMethod(), set[Group]Generic(), setAs(), setReplaceMethod()}
|
|
;; see also set-S4-exp in ess-r-function-pattern below
|
|
"^set[MGAR][GMa-z]+\\s-?(")
|
|
|
|
(defvar ess-function-pattern nil ; in R set to ess-r-function-pattern
|
|
"Regexp to match the beginning of a function in S buffers.")
|
|
|
|
(defvar ess-r-symbol-pattern
|
|
"\\(\\sw\\|\\s_\\)"
|
|
"The regular expression for matching an R symbol.")
|
|
|
|
(defvar ess-r-name-pattern
|
|
(concat "\\(" ess-r-symbol-pattern "+\\|\\(`\\).+`\\)")
|
|
"The regular expression for matching a R name.")
|
|
|
|
(defvar ess--r-s-function-pattern
|
|
(let* ((Q "\\s\"") ; quote
|
|
(Sym-0 "\\(\\sw\\|\\s_\\)") ; symbol
|
|
(Symb (concat Sym-0 "+"))
|
|
(xSymb "[^ \t\n\"']+") ;; (concat "\\[?\\[?" Sym-0 "*")); symbol / [ / [[ / [symbol / [[symbol
|
|
;; FIXME: allow '%foo%' but only when quoted; don't allow [_0-9] at beg.
|
|
(space "\\(\\s-\\|\n\\)*") ; white space
|
|
|
|
(part-1 (concat
|
|
"\\(" ;;--------outer Either-------
|
|
"\\(\\(" ; EITHER
|
|
Q xSymb Q ; any function name between quotes
|
|
"\\)\\|\\("
|
|
Symb ; (beginning of name) + ess-r-symbol-pattern
|
|
"\\)\\)")) ; END EITHER OR
|
|
|
|
(set-S4-exp
|
|
(concat
|
|
"^set\\(As\\|Method\\|Generic\\|GroupGeneric\\|ReplaceMethod\\)(" ; S4 ...
|
|
Q xSymb Q "," space
|
|
;; and now often `` signature(......), : ''
|
|
".*" ;; <<< FIXME ???
|
|
))
|
|
|
|
(part-2 (concat
|
|
"\\|" ;;--------outer Or ---------
|
|
set-S4-exp
|
|
"\\)" ;;--------end outer Either/Or-------
|
|
|
|
"\\(" space "\\s<.*\\s>\\)*" ; whitespace, comment
|
|
;; FIXME: in principle we should skip 'definition *= *' here
|
|
space "function\\s-*(" ; whitespace, function keyword, parenthesis
|
|
)))
|
|
`(,part-1 ,part-2))
|
|
"Placeholder for use in constructing `ess-r-function-pattern' and `ess-s-function-pattern'.")
|
|
|
|
(defvar ess-r-function-pattern
|
|
(concat (car ess--r-s-function-pattern)
|
|
"\\s-*\\(<-\\|=\\)" ; whitespace, assign
|
|
(nth 1 ess--r-s-function-pattern))
|
|
"The regular expression for matching the beginning of an R function.")
|
|
|
|
(defvar ess-s-function-pattern
|
|
(concat (car ess--r-s-function-pattern)
|
|
"\\s-*\\(<-\\|_\\|=\\)" ; whitespace, assign (incl. "_")
|
|
(nth 1 ess--r-s-function-pattern))
|
|
"The regular expression for matching the beginning of an S function.")
|
|
|
|
(defvar ess--fn-name-start-cache nil)
|
|
|
|
(defun ess--fn-name-start ()
|
|
"Return (FN-NAME . START-POS).
|
|
FN-NAME is a function name located before the pointer. START-POS
|
|
is the position where FN-NAME starts. Store this cons in variable
|
|
`ess--fn-name-start-cache'."
|
|
(save-excursion
|
|
(save-restriction
|
|
(let* ((proc (get-buffer-process (current-buffer)))
|
|
(mark (and proc (process-mark proc))))
|
|
(if (and mark (>= (point) mark))
|
|
(narrow-to-region mark (point)))
|
|
(and (fboundp 'ess-noweb-narrow-to-chunk)
|
|
(bound-and-true-p ess-noweb-mode)
|
|
(ess-noweb-narrow-to-chunk))
|
|
(unless (ess-inside-string-p)
|
|
(setq ess--fn-name-start-cache
|
|
(condition-case nil ;; check if it is inside a function
|
|
(progn
|
|
;; for the sake of big buffers, look only 1000 chars back
|
|
(narrow-to-region (max (point-min) (- (point) 1000)) (point))
|
|
(up-list -1)
|
|
(while (not (looking-at "("))
|
|
(up-list -1))
|
|
(let ((funname (symbol-name (ess-symbol-at-point))))
|
|
(when (and funname
|
|
(not (member funname ess-S-non-functions)))
|
|
(cons funname (- (point) (length funname))))))
|
|
(error nil))))))))
|
|
|
|
(defun ess-symbol-at-point ()
|
|
"Like `symbol-at-point' but consider fully qualified names.
|
|
Fully qualified names include accessor symbols (like aaa$bbb and
|
|
aaa@bbb in R)."
|
|
(with-syntax-table (or ess-mode-completion-syntax-table
|
|
(syntax-table))
|
|
(symbol-at-point)))
|
|
|
|
(defun ess-bounds-of-symbol ()
|
|
"Get bounds of symbol at point.
|
|
Intended for completion."
|
|
(let ((bounds (with-syntax-table (or ess-mode-completion-syntax-table
|
|
(syntax-table))
|
|
(bounds-of-thing-at-point 'symbol))))
|
|
(and bounds
|
|
(not (save-excursion
|
|
(goto-char (car bounds))
|
|
(looking-at "/\\|.[0-9]")))
|
|
bounds)))
|
|
|
|
(defun ess-symbol-start ()
|
|
"Get initial position for objects completion.
|
|
Symbols are fully qualified names that include accessor
|
|
symbols (like aaa$bbb and aaa@bbb in R)."
|
|
(car (ess-bounds-of-symbol)))
|
|
|
|
(defun ess-arg-start ()
|
|
"Get initial position for args completion."
|
|
(when (not (ess-inside-string-p))
|
|
(when (ess--fn-name-start)
|
|
(if (looking-back "[(,]+[ \t\n]*" nil)
|
|
(point)
|
|
(ess-symbol-start)))))
|
|
|
|
(defun ess-inside-string-p (&optional pos)
|
|
"Return non-nil if POS is inside string.
|
|
POS defaults to `point'."
|
|
(let ((pos (or pos (point))))
|
|
(nth 3 (syntax-ppss pos))))
|
|
|
|
(defun ess-inside-comment-p (&optional pos)
|
|
"Return non-nil if POS is inside string.
|
|
POS defaults to `point'."
|
|
(let ((pos (or pos (point))))
|
|
(nth 4 (syntax-ppss pos))))
|
|
|
|
(defun ess-inside-string-or-comment-p (&optional pos)
|
|
"Return non-nil if POS is inside a string or comment.
|
|
POS defaults to `point'."
|
|
(or (ess-inside-string-p pos)
|
|
(ess-inside-comment-p pos)))
|
|
|
|
(defun ess-inside-brackets-p (&optional pos curly?)
|
|
"Return t if position POS is inside brackets.
|
|
POS defaults to point if no value is given. If CURLY?? is non nil
|
|
also return t if inside curly brackets."
|
|
(save-excursion
|
|
(let ((ppss (syntax-ppss pos))
|
|
(r nil))
|
|
(while (and (> (nth 0 ppss) 0)
|
|
(not r))
|
|
(goto-char (nth 1 ppss))
|
|
(when (or (char-equal ?\[ (char-after))
|
|
(and curly?
|
|
(char-equal ?\{ (char-after))))
|
|
(setq r t))
|
|
(setq ppss (syntax-ppss)))
|
|
r)))
|
|
|
|
|
|
;;; String manipulation
|
|
|
|
(defun ess-quote-special-chars (string)
|
|
"Quote special characters in STRING."
|
|
(replace-regexp-in-string
|
|
"\"" "\\\\\\&"
|
|
(replace-regexp-in-string ;; replace backslashes
|
|
"\\\\" "\\\\" string nil t)))
|
|
|
|
(defun ess-rep-regexp (regexp to-string &optional fixedcase literal verbose)
|
|
"Instead of (replace-regexp..) -- do NOT replace in strings or comments.
|
|
If FIXEDCASE is non-nil, do *not* alter case of replacement text.
|
|
If LITERAL is non-nil, do *not* treat `\\' as special.
|
|
If VERBOSE is non-nil, (message ..) about replacements."
|
|
(let ((case-fold-search (and case-fold-search
|
|
(not fixedcase))); t <==> ignore case in search
|
|
(ppt (point)); previous point
|
|
(p))
|
|
(while (and (setq p (re-search-forward regexp nil t))
|
|
(< ppt p))
|
|
(setq ppt p)
|
|
(cond ((not (ess-inside-string-or-comment-p (1- p)))
|
|
(if verbose
|
|
(let ((beg (match-beginning 0)))
|
|
(message "buffer in (match-beg.,p)=(%d,%d) is '%s'"
|
|
beg p (buffer-substring beg p))))
|
|
(replace-match to-string fixedcase literal))))))
|
|
|
|
(defun ess-replace-regexp-dump-to-src (regexp to-string &optional dont-query verbose)
|
|
"Replace REGEXP matches from beginning of buffer with TO-STRING.
|
|
If DONT-QUERY is non-nil, call `ess-rep-regexp' else call
|
|
`query-replace-regexp'. VERBOSE can be passed to `ess-rep-regexp'."
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(if dont-query
|
|
(ess-rep-regexp regexp to-string nil nil verbose)
|
|
(query-replace-regexp regexp to-string nil))))
|
|
|
|
(defun ess-space-around (word &optional from verbose)
|
|
"Replace-regexp .. ensuring space around all occurrences of WORD.
|
|
Start at from FROM, which defaults to point."
|
|
(interactive "d\nP"); Defaults: point and prefix (C-u)
|
|
(save-excursion
|
|
(goto-char from)
|
|
(ess-rep-regexp (concat "\\([^ \t\n]\\)\\(\\<" word "\\>\\)")
|
|
"\\1 \\2" nil nil verbose)
|
|
(goto-char from)
|
|
(ess-rep-regexp (concat "\\(\\<" word "\\>\\)\\([^ \t\n]\\)")
|
|
"\\1 \\2" nil nil verbose)))
|
|
|
|
(defun ess-time-string (&optional clock)
|
|
"Return a string for use as a timestamp, like \"13 Mar 1992\".
|
|
Include hr:min if CLOCK is non-nil. Redefine to taste."
|
|
(declare (obsolete format-time-string "ESS 19.04"))
|
|
(format-time-string (concat "%e %b %Y" (if clock ", %H:%M"))))
|
|
|
|
(defun ess-replace-in-string (str regexp newtext &optional literal)
|
|
"Replace all matches in STR for REGEXP with NEWTEXT string.
|
|
Optional LITERAL non-nil means do a literal replacement.
|
|
Otherwise treat \\ in NEWTEXT string as special:
|
|
\\& means substitute original matched text,
|
|
\\N means substitute match for \(...\) number N,
|
|
\\\\ means insert one \\."
|
|
(if (not (stringp str))
|
|
(error "(replace-in-string): First argument must be a string: %s" str))
|
|
(if (stringp newtext)
|
|
nil
|
|
(error "(replace-in-string): 3rd arg must be a string: %s"
|
|
newtext))
|
|
(let ((rtn-str "")
|
|
(start 0)
|
|
(special)
|
|
match prev-start)
|
|
(while (setq match (string-match regexp str start))
|
|
(setq prev-start start
|
|
start (match-end 0)
|
|
rtn-str
|
|
(concat
|
|
rtn-str
|
|
(substring str prev-start match)
|
|
(cond (literal newtext)
|
|
(t (mapconcat
|
|
(function
|
|
(lambda (c)
|
|
(if special
|
|
(progn
|
|
(setq special nil)
|
|
(cond ((eq c ?\\) "\\")
|
|
((eq c ?&)
|
|
(substring str
|
|
(match-beginning 0)
|
|
(match-end 0)))
|
|
((and (>= c ?0) (<= c ?9))
|
|
(if (> c (+ ?0 (length
|
|
(match-data))))
|
|
;; Invalid match num
|
|
(error "(replace-in-string) Invalid match num: %c" c)
|
|
(setq c (- c ?0))
|
|
(substring str
|
|
(match-beginning c)
|
|
(match-end c))))
|
|
(t (char-to-string c))))
|
|
(if (eq c ?\\) (progn (setq special t) nil)
|
|
(char-to-string c)))))
|
|
newtext ""))))))
|
|
(concat rtn-str (substring str start))))
|
|
|
|
;;- From: friedman@gnu.ai.mit.edu (Noah Friedman)
|
|
;;- Date: 12 Feb 1995 21:30:56 -0500
|
|
;;- Newsgroups: gnu.emacs.sources
|
|
;;- Subject: nuke-trailing-whitespace
|
|
;;-
|
|
;;- This is too trivial to make into a big todo with comments and copyright
|
|
;;- notices whose length exceed the size of the actual code, so consider it
|
|
;;- public domain. Its purpose is along similar lines to that of
|
|
;;- `require-final-newline', which is built in. I hope the names make it
|
|
;;- obvious.
|
|
|
|
;; (add-hook 'write-file-hooks 'nuke-trailing-whitespace)
|
|
;;or at least
|
|
;; (add-hook 'ess-mode-hook
|
|
;; (lambda ()
|
|
;; (add-hook 'local-write-file-hooks 'nuke-trailing-whitespace)))
|
|
|
|
(defvar ess-nuke-trailing-whitespace-p nil;disabled by default 'ask
|
|
"[Dis]activates (ess-nuke-trailing-whitespace).
|
|
Disabled if nil; if t, it works unconditionally, otherwise,
|
|
the user is queried.
|
|
Note that setting the default to t may not be a good idea when you edit
|
|
binary files!")
|
|
|
|
;;; MM: Newer Emacsen now have delete-trailing-whitespace
|
|
;;; -- but no customization like nuke-trailing-whitespace-p ..
|
|
(defun ess-nuke-trailing-whitespace ()
|
|
"Nuke all trailing whitespace in the buffer.
|
|
Whitespace in this case is just spaces or tabs. This is a useful
|
|
function to put on `write-file-hooks'. If the variable
|
|
`ess-nuke-trailing-whitespace-p' is nil, this function is
|
|
disabled. If t, unreservedly strip trailing whitespace. If not
|
|
nil and not t, query for each instance."
|
|
(interactive)
|
|
(let ((bname (buffer-name)))
|
|
(cond ((or
|
|
(string= major-mode "rmail-mode")
|
|
(string= bname "RMAIL")
|
|
nil)); do nothing..
|
|
(t
|
|
(and (not buffer-read-only)
|
|
ess-nuke-trailing-whitespace-p
|
|
(save-match-data
|
|
(save-excursion
|
|
(save-restriction
|
|
(widen)
|
|
(goto-char (point-min))
|
|
(cond ((eq ess-nuke-trailing-whitespace-p t)
|
|
(while (re-search-forward "[ \t]+$" (point-max) t)
|
|
(delete-region (match-beginning 0)
|
|
(match-end 0))))
|
|
(t
|
|
(query-replace-regexp "[ \t]+$" "")))))))))
|
|
;; always return nil, in case this is on write-file-hooks.
|
|
nil))
|
|
|
|
|
|
;;; Debugging tools
|
|
|
|
(defun ess-write-to-dribble-buffer (text)
|
|
"Write TEXT to `ess-dribble-buffer'."
|
|
(when (or ess-verbose ess-write-to-dribble)
|
|
(with-current-buffer (get-buffer-create ess-dribble-buffer)
|
|
(goto-char (point-max))
|
|
(insert-before-markers text))))
|
|
|
|
(defun ess-if-verbose-write (text)
|
|
"Write TEXT to `ess-dribble-buffer' only if `ess-verbose' is non-nil."
|
|
(when ess-verbose (ess-write-to-dribble-buffer text)))
|
|
|
|
(defun ess-kill-last-line ()
|
|
(save-excursion
|
|
(goto-char (point-max))
|
|
(forward-line -1)
|
|
(delete-region (point-at-eol) (point-max))))
|
|
|
|
(defun ess-sleep ()
|
|
"Put Emacs to sleep for `ess-sleep-for-shell' seconds (floats work)."
|
|
(sleep-for ess-sleep-for-shell))
|
|
|
|
(defun ess-setq-vars-local (alist &optional buf)
|
|
"Set language variables from ALIST, in buffer BUF, if desired."
|
|
(when buf (set-buffer buf))
|
|
(mapc (lambda (pair)
|
|
(make-local-variable (car pair))
|
|
(set (car pair) (eval (cdr pair)))
|
|
(when (bound-and-true-p ess--make-local-vars-permanent)
|
|
(put (car pair) 'permanent-local t))) ;; hack for Rnw
|
|
alist))
|
|
|
|
(defvar ess-error-regexp "^\\(Syntax error: .*\\) at line \\([0-9]*\\), file \\(.*\\)$"
|
|
"Regexp to search for errors.")
|
|
|
|
(define-obsolete-function-alias 'ess-beginning-of-function 'beginning-of-defun "ESS 19.04")
|
|
(define-obsolete-function-alias 'ess-end-of-function 'end-of-defun "ESS 19.04")
|
|
|
|
(provide 'ess-utils)
|
|
|
|
;;; ess-utils.el ends here
|